feat: handle charging profile purpose TxProfile
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
CommitLineData
66a7748d
JB
1import { createHash, randomBytes } from 'node:crypto'
2import type { EventEmitter } from 'node:events'
ffb833ec 3import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path'
66a7748d
JB
4import { env } from 'node:process'
5import { fileURLToPath } from 'node:url'
8114d10e 6
66a7748d 7import chalk from 'chalk'
f924d466
JB
8import {
9 addDays,
f924d466
JB
10 addSeconds,
11 addWeeks,
497588ef 12 differenceInDays,
d476bc1b 13 differenceInSeconds,
497588ef 14 differenceInWeeks,
4c3f6c20 15 type Interval,
f924d466
JB
16 isAfter,
17 isBefore,
0bd926c1 18 isDate,
90aceaf6 19 isPast,
252a7d22 20 isWithinInterval,
66a7748d
JB
21 toDate
22} from 'date-fns'
23import { maxTime } from 'date-fns/constants'
38ae4ce2 24import { isEmpty } from 'rambda'
8114d10e 25
66a7748d 26import { BaseError } from '../exception/index.js'
981ebfbe 27import {
492cf6ab 28 AmpereUnits,
04b1261c 29 AvailabilityType,
268a74bb
JB
30 type BootNotificationRequest,
31 BootReasonEnumType,
15068be9 32 type ChargingProfile,
268a74bb 33 ChargingProfileKindType,
65099c1e 34 ChargingProfilePurposeType,
15068be9
JB
35 ChargingRateUnitType,
36 type ChargingSchedulePeriod,
73edcc94 37 type ChargingStationConfiguration,
268a74bb 38 type ChargingStationInfo,
36b73d95 39 type ChargingStationOptions,
268a74bb 40 type ChargingStationTemplate,
66a7748d 41 type ChargingStationWorkerMessageEvents,
dd08d43d 42 ConnectorPhaseRotation,
a78ef5ed 43 type ConnectorStatus,
c3b83130 44 ConnectorStatusEnum,
268a74bb 45 CurrentType,
ae25f265 46 type EvseTemplate,
268a74bb
JB
47 type OCPP16BootNotificationRequest,
48 type OCPP20BootNotificationRequest,
49 OCPPVersion,
50 RecurrencyKindType,
90aceaf6
JB
51 type Reservation,
52 ReservationTerminationReason,
d8093be1 53 StandardParametersKey,
66a7748d
JB
54 type SupportedFeatureProfiles,
55 Voltage
56} from '../types/index.js'
9bf0ef23
JB
57import {
58 ACElectricUtils,
40615072 59 clone,
4c3f6c20 60 Constants,
b85cef4c 61 convertToDate,
9bf0ef23 62 convertToInt,
4c3f6c20 63 DCElectricUtils,
80c58041 64 isArraySorted,
9bf0ef23
JB
65 isNotEmptyArray,
66 isNotEmptyString,
5dc7c990 67 isValidDate,
9bf0ef23 68 logger,
66a7748d
JB
69 secureRandom
70} from '../utils/index.js'
4c3f6c20
JB
71import type { ChargingStation } from './ChargingStation.js'
72import { getConfigurationKey } from './ConfigurationKeyUtils.js'
17ac262c 73
66a7748d 74const moduleName = 'Helpers'
91a4f151 75
ffb833ec
JB
76export const buildTemplateName = (templateFile: string): string => {
77 if (isAbsolute(templateFile)) {
78 templateFile = relative(
79 resolve(join(dirname(fileURLToPath(import.meta.url)), 'assets', 'station-templates')),
80 templateFile
81 )
82 }
83 const templateFileParsedPath = parse(templateFile)
84 return join(templateFileParsedPath.dir, templateFileParsedPath.name)
85}
86
fba11dc6
JB
87export const getChargingStationId = (
88 index: number,
66a7748d 89 stationTemplate: ChargingStationTemplate | undefined
fba11dc6 90): string => {
a807045b 91 if (stationTemplate == null) {
66a7748d 92 return "Unknown 'chargingStationId'"
c1f16afd 93 }
fba11dc6 94 // In case of multiple instances: add instance index to charging station id
66a7748d 95 const instanceIndex = env.CF_INSTANCE_INDEX ?? 0
5199f9fd 96 const idSuffix = stationTemplate.nameSuffix ?? ''
66a7748d 97 const idStr = `000000000${index.toString()}`
5199f9fd 98 return stationTemplate.fixedName === true
fba11dc6 99 ? stationTemplate.baseName
f1bd9d19 100 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
66a7748d
JB
101 idStr.length - 4
102 )}${idSuffix}`
103}
fba11dc6 104
90aceaf6 105export const hasReservationExpired = (reservation: Reservation): boolean => {
66a7748d
JB
106 return isPast(reservation.expiryDate)
107}
90aceaf6
JB
108
109export const removeExpiredReservations = async (
66a7748d 110 chargingStation: ChargingStation
90aceaf6
JB
111): Promise<void> => {
112 if (chargingStation.hasEvses) {
113 for (const evseStatus of chargingStation.evses.values()) {
114 for (const connectorStatus of evseStatus.connectors.values()) {
66a7748d
JB
115 if (
116 connectorStatus.reservation != null &&
117 hasReservationExpired(connectorStatus.reservation)
118 ) {
90aceaf6
JB
119 await chargingStation.removeReservation(
120 connectorStatus.reservation,
66a7748d
JB
121 ReservationTerminationReason.EXPIRED
122 )
90aceaf6
JB
123 }
124 }
125 }
126 } else {
127 for (const connectorStatus of chargingStation.connectors.values()) {
66a7748d
JB
128 if (
129 connectorStatus.reservation != null &&
130 hasReservationExpired(connectorStatus.reservation)
131 ) {
90aceaf6
JB
132 await chargingStation.removeReservation(
133 connectorStatus.reservation,
66a7748d
JB
134 ReservationTerminationReason.EXPIRED
135 )
90aceaf6
JB
136 }
137 }
138 }
66a7748d 139}
90aceaf6
JB
140
141export const getNumberOfReservableConnectors = (
66a7748d 142 connectors: Map<number, ConnectorStatus>
90aceaf6 143): number => {
66a7748d 144 let numberOfReservableConnectors = 0
fba11dc6
JB
145 for (const [connectorId, connectorStatus] of connectors) {
146 if (connectorId === 0) {
66a7748d 147 continue
3fa7f799 148 }
fba11dc6 149 if (connectorStatus.status === ConnectorStatusEnum.Available) {
66a7748d 150 ++numberOfReservableConnectors
1bf29f5b 151 }
1bf29f5b 152 }
66a7748d
JB
153 return numberOfReservableConnectors
154}
fba11dc6
JB
155
156export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
157 const chargingStationInfo = {
158 chargePointModel: stationTemplate.chargePointModel,
159 chargePointVendor: stationTemplate.chargePointVendor,
d760a0a6 160 ...(stationTemplate.chargeBoxSerialNumberPrefix != null && {
66a7748d 161 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix
fba11dc6 162 }),
d760a0a6 163 ...(stationTemplate.chargePointSerialNumberPrefix != null && {
66a7748d 164 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
fba11dc6 165 }),
d760a0a6 166 ...(stationTemplate.meterSerialNumberPrefix != null && {
66a7748d 167 meterSerialNumber: stationTemplate.meterSerialNumberPrefix
fba11dc6 168 }),
d760a0a6 169 ...(stationTemplate.meterType != null && {
66a7748d
JB
170 meterType: stationTemplate.meterType
171 })
172 }
fba11dc6
JB
173 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
174 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
66a7748d
JB
175 .digest('hex')
176}
fba11dc6
JB
177
178export const checkChargingStation = (
179 chargingStation: ChargingStation,
66a7748d 180 logPrefix: string
fba11dc6 181): boolean => {
66a7748d
JB
182 if (!chargingStation.started && !chargingStation.starting) {
183 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`)
184 return false
fba11dc6 185 }
66a7748d
JB
186 return true
187}
fba11dc6
JB
188
189export const getPhaseRotationValue = (
190 connectorId: number,
66a7748d 191 numberOfPhases: number
fba11dc6
JB
192): string | undefined => {
193 // AC/DC
194 if (connectorId === 0 && numberOfPhases === 0) {
66a7748d 195 return `${connectorId}.${ConnectorPhaseRotation.RST}`
fba11dc6 196 } else if (connectorId > 0 && numberOfPhases === 0) {
66a7748d 197 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
fba11dc6 198 // AC
d181b12b 199 } else if (connectorId >= 0 && numberOfPhases === 1) {
66a7748d 200 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
d181b12b 201 } else if (connectorId >= 0 && numberOfPhases === 3) {
66a7748d 202 return `${connectorId}.${ConnectorPhaseRotation.RST}`
fba11dc6 203 }
66a7748d 204}
fba11dc6 205
5199f9fd 206export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate> | undefined): number => {
66a7748d
JB
207 if (evses == null) {
208 return -1
fba11dc6 209 }
66a7748d
JB
210 return Object.keys(evses).length
211}
fba11dc6 212
5199f9fd
JB
213const getMaxNumberOfConnectors = (
214 connectors: Record<string, ConnectorStatus> | undefined
215): number => {
66a7748d
JB
216 if (connectors == null) {
217 return -1
fba11dc6 218 }
66a7748d
JB
219 return Object.keys(connectors).length
220}
fba11dc6
JB
221
222export const getBootConnectorStatus = (
223 chargingStation: ChargingStation,
224 connectorId: number,
66a7748d 225 connectorStatus: ConnectorStatus
fba11dc6 226): ConnectorStatusEnum => {
66a7748d 227 let connectorBootStatus: ConnectorStatusEnum
fba11dc6 228 if (
5199f9fd 229 connectorStatus.status == null &&
66a7748d
JB
230 (!chargingStation.isChargingStationAvailable() ||
231 !chargingStation.isConnectorAvailable(connectorId))
fba11dc6 232 ) {
66a7748d 233 connectorBootStatus = ConnectorStatusEnum.Unavailable
5199f9fd 234 } else if (connectorStatus.status == null && connectorStatus.bootStatus != null) {
fba11dc6 235 // Set boot status in template at startup
5199f9fd
JB
236 connectorBootStatus = connectorStatus.bootStatus
237 } else if (connectorStatus.status != null) {
fba11dc6 238 // Set previous status at startup
5199f9fd 239 connectorBootStatus = connectorStatus.status
fba11dc6
JB
240 } else {
241 // Set default status
66a7748d 242 connectorBootStatus = ConnectorStatusEnum.Available
fba11dc6 243 }
66a7748d
JB
244 return connectorBootStatus
245}
fba11dc6
JB
246
247export const checkTemplate = (
5199f9fd 248 stationTemplate: ChargingStationTemplate | undefined,
fba11dc6 249 logPrefix: string,
66a7748d 250 templateFile: string
fba11dc6 251): void => {
66a7748d
JB
252 if (stationTemplate == null) {
253 const errorMsg = `Failed to read charging station template file ${templateFile}`
254 logger.error(`${logPrefix} ${errorMsg}`)
255 throw new BaseError(errorMsg)
fba11dc6 256 }
38ae4ce2 257 if (isEmpty(stationTemplate)) {
66a7748d
JB
258 const errorMsg = `Empty charging station information from template file ${templateFile}`
259 logger.error(`${logPrefix} ${errorMsg}`)
260 throw new BaseError(errorMsg)
fba11dc6 261 }
38ae4ce2 262 if (stationTemplate.idTagsFile == null || isEmpty(stationTemplate.idTagsFile)) {
fba11dc6 263 logger.warn(
66a7748d
JB
264 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
265 )
c3b83130 266 }
66a7748d 267}
fba11dc6 268
73edcc94
JB
269export const checkConfiguration = (
270 stationConfiguration: ChargingStationConfiguration | undefined,
271 logPrefix: string,
66a7748d 272 configurationFile: string
73edcc94 273): void => {
a807045b 274 if (stationConfiguration == null) {
66a7748d
JB
275 const errorMsg = `Failed to read charging station configuration file ${configurationFile}`
276 logger.error(`${logPrefix} ${errorMsg}`)
277 throw new BaseError(errorMsg)
73edcc94 278 }
38ae4ce2 279 if (isEmpty(stationConfiguration)) {
66a7748d
JB
280 const errorMsg = `Empty charging station configuration from file ${configurationFile}`
281 logger.error(`${logPrefix} ${errorMsg}`)
282 throw new BaseError(errorMsg)
73edcc94 283 }
66a7748d 284}
73edcc94 285
fba11dc6
JB
286export const checkConnectorsConfiguration = (
287 stationTemplate: ChargingStationTemplate,
288 logPrefix: string,
66a7748d 289 templateFile: string
fba11dc6 290): {
66a7748d
JB
291 configuredMaxConnectors: number
292 templateMaxConnectors: number
293 templateMaxAvailableConnectors: number
fba11dc6 294} => {
66a7748d
JB
295 const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate)
296 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile)
5199f9fd 297 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors)
66a7748d
JB
298 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile)
299 const templateMaxAvailableConnectors =
300 stationTemplate.Connectors?.[0] != null ? templateMaxConnectors - 1 : templateMaxConnectors
fba11dc6
JB
301 if (
302 configuredMaxConnectors > templateMaxAvailableConnectors &&
5199f9fd 303 stationTemplate.randomConnectors !== true
8a133cc8 304 ) {
fba11dc6 305 logger.warn(
66a7748d
JB
306 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
307 )
308 stationTemplate.randomConnectors = true
fba11dc6 309 }
48847bc0
JB
310 return {
311 configuredMaxConnectors,
312 templateMaxConnectors,
313 templateMaxAvailableConnectors
314 }
66a7748d 315}
fba11dc6
JB
316
317export const checkStationInfoConnectorStatus = (
318 connectorId: number,
319 connectorStatus: ConnectorStatus,
320 logPrefix: string,
66a7748d 321 templateFile: string
fba11dc6 322): void => {
5199f9fd 323 if (connectorStatus.status != null) {
fba11dc6 324 logger.warn(
66a7748d
JB
325 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
326 )
327 delete connectorStatus.status
fba11dc6 328 }
66a7748d 329}
fba11dc6 330
36b73d95
JB
331export const setChargingStationOptions = (
332 stationInfo: ChargingStationInfo,
333 options?: ChargingStationOptions
334): ChargingStationInfo => {
afbb8202 335 if (options?.supervisionUrls != null) {
2293fadc 336 stationInfo.supervisionUrls = options.supervisionUrls
afbb8202 337 }
36b73d95 338 if (options?.persistentConfiguration != null) {
c2be11fd 339 stationInfo.stationInfoPersistentConfiguration = options.persistentConfiguration
36b73d95
JB
340 stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
341 stationInfo.automaticTransactionGeneratorPersistentConfiguration =
342 options.persistentConfiguration
343 }
1253d60c
JB
344 if (options?.autoStart != null) {
345 stationInfo.autoStart = options.autoStart
346 }
36b73d95
JB
347 if (options?.autoRegister != null) {
348 stationInfo.autoRegister = options.autoRegister
349 }
350 if (options?.enableStatistics != null) {
351 stationInfo.enableStatistics = options.enableStatistics
352 }
353 if (options?.ocppStrictCompliance != null) {
354 stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
355 }
356 if (options?.stopTransactionsOnStopped != null) {
357 stationInfo.stopTransactionsOnStopped = options.stopTransactionsOnStopped
358 }
359 return stationInfo
360}
361
1fa9df8c
JB
362export const buildConnectorsMap = (
363 connectors: Record<string, ConnectorStatus>,
364 logPrefix: string,
365 templateFile: string
366): Map<number, ConnectorStatus> => {
367 const connectorsMap = new Map<number, ConnectorStatus>()
368 if (getMaxNumberOfConnectors(connectors) > 0) {
369 for (const connector in connectors) {
370 const connectorStatus = connectors[connector]
371 const connectorId = convertToInt(connector)
372 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
373 connectorsMap.set(connectorId, clone<ConnectorStatus>(connectorStatus))
374 }
375 } else {
376 logger.warn(
377 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
378 )
379 }
380 return connectorsMap
381}
382
fba11dc6
JB
383export const initializeConnectorsMapStatus = (
384 connectors: Map<number, ConnectorStatus>,
66a7748d 385 logPrefix: string
fba11dc6
JB
386): void => {
387 for (const connectorId of connectors.keys()) {
388 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
04b1261c 389 logger.warn(
a223d9be
JB
390 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
391 connectors.get(connectorId)?.transactionId
392 }`
66a7748d 393 )
04b1261c 394 }
fba11dc6 395 if (connectorId === 0) {
66a7748d
JB
396 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
397 connectors.get(connectorId)!.availability = AvailabilityType.Operative
300418e9 398 if (connectors.get(connectorId)?.chargingProfiles == null) {
66a7748d
JB
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 connectors.get(connectorId)!.chargingProfiles = []
04b1261c 401 }
be9f397b 402 } else if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted == null) {
66a7748d
JB
403 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
404 initializeConnectorStatus(connectors.get(connectorId)!)
04b1261c
JB
405 }
406 }
66a7748d 407}
fba11dc6 408
239194e9
JB
409export const resetAuthorizeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
410 connectorStatus.idTagLocalAuthorized = false
411 connectorStatus.idTagAuthorized = false
412 delete connectorStatus.localAuthorizeIdTag
413 delete connectorStatus.authorizeIdTag
414}
415
f938317f
JB
416export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefined): void => {
417 if (connectorStatus == null) {
418 return
419 }
626d3ce5
JB
420 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
421 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles.filter(
422 chargingProfile =>
65099c1e 423 chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE ||
626d3ce5
JB
424 (chargingProfile.transactionId != null &&
425 connectorStatus.transactionId != null &&
65099c1e 426 chargingProfile.transactionId !== connectorStatus.transactionId)
626d3ce5
JB
427 )
428 }
239194e9 429 resetAuthorizeConnectorStatus(connectorStatus)
66a7748d
JB
430 connectorStatus.transactionRemoteStarted = false
431 connectorStatus.transactionStarted = false
5199f9fd
JB
432 delete connectorStatus.transactionStart
433 delete connectorStatus.transactionId
5199f9fd 434 delete connectorStatus.transactionIdTag
66a7748d 435 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
5199f9fd 436 delete connectorStatus.transactionBeginMeterValue
66a7748d 437}
fba11dc6 438
1fa9df8c
JB
439export const prepareDatesInConnectorStatus = (
440 connectorStatus: ConnectorStatus
441): ConnectorStatus => {
442 if (connectorStatus.reservation != null) {
443 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
444 connectorStatus.reservation.expiryDate = convertToDate(connectorStatus.reservation.expiryDate)!
445 }
446 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
447 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles.map(chargingProfile => {
448 chargingProfile.chargingSchedule.startSchedule = convertToDate(
449 chargingProfile.chargingSchedule.startSchedule
450 )
451 chargingProfile.validFrom = convertToDate(chargingProfile.validFrom)
452 chargingProfile.validTo = convertToDate(chargingProfile.validTo)
453 return chargingProfile
454 })
455 }
456 return connectorStatus
457}
458
fba11dc6
JB
459export const createBootNotificationRequest = (
460 stationInfo: ChargingStationInfo,
66a7748d 461 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
2466918c
JB
462): BootNotificationRequest | undefined => {
463 const ocppVersion = stationInfo.ocppVersion
fba11dc6
JB
464 switch (ocppVersion) {
465 case OCPPVersion.VERSION_16:
466 return {
467 chargePointModel: stationInfo.chargePointModel,
468 chargePointVendor: stationInfo.chargePointVendor,
d760a0a6 469 ...(stationInfo.chargeBoxSerialNumber != null && {
66a7748d 470 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
fba11dc6 471 }),
d760a0a6 472 ...(stationInfo.chargePointSerialNumber != null && {
66a7748d 473 chargePointSerialNumber: stationInfo.chargePointSerialNumber
fba11dc6 474 }),
d760a0a6 475 ...(stationInfo.firmwareVersion != null && {
66a7748d 476 firmwareVersion: stationInfo.firmwareVersion
fba11dc6 477 }),
d760a0a6
JB
478 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
479 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
480 ...(stationInfo.meterSerialNumber != null && {
66a7748d 481 meterSerialNumber: stationInfo.meterSerialNumber
fba11dc6 482 }),
d760a0a6 483 ...(stationInfo.meterType != null && {
66a7748d
JB
484 meterType: stationInfo.meterType
485 })
486 } satisfies OCPP16BootNotificationRequest
fba11dc6
JB
487 case OCPPVersion.VERSION_20:
488 case OCPPVersion.VERSION_201:
489 return {
490 reason: bootReason,
491 chargingStation: {
492 model: stationInfo.chargePointModel,
493 vendorName: stationInfo.chargePointVendor,
d760a0a6 494 ...(stationInfo.firmwareVersion != null && {
66a7748d 495 firmwareVersion: stationInfo.firmwareVersion
d270cc87 496 }),
d760a0a6 497 ...(stationInfo.chargeBoxSerialNumber != null && {
66a7748d 498 serialNumber: stationInfo.chargeBoxSerialNumber
d270cc87 499 }),
d760a0a6 500 ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
fba11dc6 501 modem: {
d760a0a6
JB
502 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
503 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi })
66a7748d
JB
504 }
505 })
506 }
507 } satisfies OCPP20BootNotificationRequest
fba11dc6 508 }
66a7748d 509}
fba11dc6
JB
510
511export const warnTemplateKeysDeprecation = (
512 stationTemplate: ChargingStationTemplate,
513 logPrefix: string,
66a7748d
JB
514 templateFile: string
515): void => {
516 const templateKeys: Array<{ deprecatedKey: string, key?: string }> = [
e4c6cf05
JB
517 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
518 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
519 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
66a7748d
JB
520 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' }
521 ]
fba11dc6
JB
522 for (const templateKey of templateKeys) {
523 warnDeprecatedTemplateKey(
524 stationTemplate,
525 templateKey.deprecatedKey,
526 logPrefix,
527 templateFile,
d760a0a6 528 templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
66a7748d
JB
529 )
530 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
fba11dc6 531 }
66a7748d 532}
fba11dc6
JB
533
534export const stationTemplateToStationInfo = (
66a7748d 535 stationTemplate: ChargingStationTemplate
fba11dc6 536): ChargingStationInfo => {
40615072 537 stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
66a7748d
JB
538 delete stationTemplate.power
539 delete stationTemplate.powerUnit
540 delete stationTemplate.Connectors
541 delete stationTemplate.Evses
542 delete stationTemplate.Configuration
543 delete stationTemplate.AutomaticTransactionGenerator
e566a667 544 delete stationTemplate.numberOfConnectors
66a7748d
JB
545 delete stationTemplate.chargeBoxSerialNumberPrefix
546 delete stationTemplate.chargePointSerialNumberPrefix
547 delete stationTemplate.meterSerialNumberPrefix
548 return stationTemplate as ChargingStationInfo
549}
fba11dc6
JB
550
551export const createSerialNumber = (
552 stationTemplate: ChargingStationTemplate,
553 stationInfo: ChargingStationInfo,
7f3decca 554 params?: {
66a7748d
JB
555 randomSerialNumberUpperCase?: boolean
556 randomSerialNumber?: boolean
557 }
fba11dc6 558): void => {
48847bc0
JB
559 params = {
560 ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true },
561 ...params
562 }
66a7748d 563 const serialNumberSuffix =
5199f9fd 564 params.randomSerialNumber === true
66a7748d
JB
565 ? getRandomSerialNumberSuffix({
566 upperCase: params.randomSerialNumberUpperCase
fba11dc6 567 })
66a7748d 568 : ''
5199f9fd 569 isNotEmptyString(stationTemplate.chargePointSerialNumberPrefix) &&
66a7748d 570 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
5199f9fd 571 isNotEmptyString(stationTemplate.chargeBoxSerialNumberPrefix) &&
66a7748d 572 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
5199f9fd 573 isNotEmptyString(stationTemplate.meterSerialNumberPrefix) &&
66a7748d
JB
574 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
575}
fba11dc6
JB
576
577export const propagateSerialNumber = (
5199f9fd
JB
578 stationTemplate: ChargingStationTemplate | undefined,
579 stationInfoSrc: ChargingStationInfo | undefined,
66a7748d
JB
580 stationInfoDst: ChargingStationInfo
581): void => {
582 if (stationInfoSrc == null || stationTemplate == null) {
fba11dc6 583 throw new BaseError(
66a7748d
JB
584 'Missing charging station template or existing configuration to propagate serial number'
585 )
17ac262c 586 }
5199f9fd
JB
587 stationTemplate.chargePointSerialNumberPrefix != null &&
588 stationInfoSrc.chargePointSerialNumber != null
fba11dc6 589 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
5199f9fd 590 : stationInfoDst.chargePointSerialNumber != null &&
66a7748d 591 delete stationInfoDst.chargePointSerialNumber
5199f9fd
JB
592 stationTemplate.chargeBoxSerialNumberPrefix != null &&
593 stationInfoSrc.chargeBoxSerialNumber != null
fba11dc6 594 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
5199f9fd
JB
595 : stationInfoDst.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
596 stationTemplate.meterSerialNumberPrefix != null && stationInfoSrc.meterSerialNumber != null
fba11dc6 597 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
5199f9fd 598 : stationInfoDst.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
66a7748d 599}
fba11dc6 600
d8093be1
JB
601export const hasFeatureProfile = (
602 chargingStation: ChargingStation,
66a7748d 603 featureProfile: SupportedFeatureProfiles
d8093be1
JB
604): boolean | undefined => {
605 return getConfigurationKey(
606 chargingStation,
66a7748d
JB
607 StandardParametersKey.SupportedFeatureProfiles
608 )?.value?.includes(featureProfile)
609}
d8093be1 610
fba11dc6 611export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
66a7748d 612 let unitDivider = 1
fba11dc6
JB
613 switch (stationInfo.amperageLimitationUnit) {
614 case AmpereUnits.DECI_AMPERE:
66a7748d
JB
615 unitDivider = 10
616 break
fba11dc6 617 case AmpereUnits.CENTI_AMPERE:
66a7748d
JB
618 unitDivider = 100
619 break
fba11dc6 620 case AmpereUnits.MILLI_AMPERE:
66a7748d
JB
621 unitDivider = 1000
622 break
fba11dc6 623 }
66a7748d
JB
624 return unitDivider
625}
fba11dc6 626
6fc0c6f3 627/**
7abb61bb 628 * Gets the connector charging profiles relevant for power limitation shallow cloned and sorted by priorities
6fc0c6f3 629 *
7abb61bb
JB
630 * @param chargingStation - Charging station
631 * @param connectorId - Connector id
6fc0c6f3
JB
632 * @returns connector charging profiles array
633 */
634export const getConnectorChargingProfiles = (
635 chargingStation: ChargingStation,
66a7748d
JB
636 connectorId: number
637): ChargingProfile[] => {
7abb61bb 638 // FIXME: handle charging profile purpose CHARGE_POINT_MAX_PROFILE
a629e6fc
JB
639 return (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
640 .slice()
7abb61bb
JB
641 .sort((a, b) => {
642 if (
643 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE &&
644 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
645 ) {
646 return -1
647 } else if (
648 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE &&
649 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE
650 ) {
651 return 1
652 }
653 return b.stackLevel - a.stackLevel
654 })
a629e6fc
JB
655 .concat(
656 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [])
7abb61bb
JB
657 .filter(
658 chargingProfile =>
659 chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
660 )
a629e6fc
JB
661 .sort((a, b) => b.stackLevel - a.stackLevel)
662 )
66a7748d 663}
6fc0c6f3 664
fba11dc6
JB
665export const getChargingStationConnectorChargingProfilesPowerLimit = (
666 chargingStation: ChargingStation,
66a7748d 667 connectorId: number
fba11dc6 668): number | undefined => {
66a7748d 669 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
66a7748d 670 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
fba11dc6 671 if (isNotEmptyArray(chargingProfiles)) {
a71d4e70
JB
672 const result = getLimitFromChargingProfiles(
673 chargingStation,
674 connectorId,
675 chargingProfiles,
66a7748d
JB
676 chargingStation.logPrefix()
677 )
be9f397b 678 if (result != null) {
5199f9fd
JB
679 limit = result.limit
680 chargingProfile = result.chargingProfile
5398cecf 681 switch (chargingStation.stationInfo?.currentOutType) {
fba11dc6
JB
682 case CurrentType.AC:
683 limit =
5199f9fd 684 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6
JB
685 ? limit
686 : ACElectricUtils.powerTotal(
66a7748d
JB
687 chargingStation.getNumberOfPhases(),
688 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
689 chargingStation.stationInfo.voltageOut!,
be9f397b 690 limit
66a7748d
JB
691 )
692 break
fba11dc6
JB
693 case CurrentType.DC:
694 limit =
5199f9fd 695 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6 696 ? limit
66a7748d 697 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
be9f397b 698 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
fba11dc6
JB
699 }
700 const connectorMaximumPower =
66a7748d 701 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 702 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
be9f397b 703 if (limit > connectorMaximumPower) {
fba11dc6 704 logger.error(
5199f9fd
JB
705 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
706 chargingProfile.chargingProfileId
707 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
66a7748d
JB
708 result
709 )
710 limit = connectorMaximumPower
15068be9
JB
711 }
712 }
15068be9 713 }
66a7748d
JB
714 return limit
715}
fba11dc6
JB
716
717export const getDefaultVoltageOut = (
718 currentType: CurrentType,
719 logPrefix: string,
66a7748d 720 templateFile: string
fba11dc6 721): Voltage => {
66a7748d
JB
722 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
723 let defaultVoltageOut: number
fba11dc6
JB
724 switch (currentType) {
725 case CurrentType.AC:
66a7748d
JB
726 defaultVoltageOut = Voltage.VOLTAGE_230
727 break
fba11dc6 728 case CurrentType.DC:
66a7748d
JB
729 defaultVoltageOut = Voltage.VOLTAGE_400
730 break
fba11dc6 731 default:
66a7748d
JB
732 logger.error(`${logPrefix} ${errorMsg}`)
733 throw new BaseError(errorMsg)
15068be9 734 }
66a7748d
JB
735 return defaultVoltageOut
736}
fba11dc6
JB
737
738export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
66a7748d
JB
739 return stationInfo.idTagsFile != null
740 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
741 : undefined
742}
fba11dc6 743
b2b60626 744export const waitChargingStationEvents = async (
fba11dc6
JB
745 emitter: EventEmitter,
746 event: ChargingStationWorkerMessageEvents,
66a7748d 747 eventsToWait: number
fba11dc6 748): Promise<number> => {
a974c8e4 749 return await new Promise<number>(resolve => {
66a7748d 750 let events = 0
fba11dc6 751 if (eventsToWait === 0) {
66a7748d
JB
752 resolve(events)
753 return
fba11dc6
JB
754 }
755 emitter.on(event, () => {
66a7748d 756 ++events
fba11dc6 757 if (events === eventsToWait) {
66a7748d 758 resolve(events)
b1f1b0f6 759 }
66a7748d
JB
760 })
761 })
762}
fba11dc6 763
cfc9875a 764const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
66a7748d
JB
765 let configuredMaxNumberOfConnectors = 0
766 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
5dc7c990 767 const numberOfConnectors = stationTemplate.numberOfConnectors
66a7748d
JB
768 configuredMaxNumberOfConnectors =
769 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
300418e9 770 } else if (stationTemplate.numberOfConnectors != null) {
5dc7c990 771 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
66a7748d 772 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
cfc9875a 773 configuredMaxNumberOfConnectors =
5199f9fd
JB
774 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
775 stationTemplate.Connectors[0] != null
66a7748d
JB
776 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
777 : getMaxNumberOfConnectors(stationTemplate.Connectors)
778 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
fba11dc6
JB
779 for (const evse in stationTemplate.Evses) {
780 if (evse === '0') {
66a7748d 781 continue
cda5d0fb 782 }
cfc9875a 783 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
66a7748d
JB
784 stationTemplate.Evses[evse].Connectors
785 )
cda5d0fb 786 }
cda5d0fb 787 }
66a7748d
JB
788 return configuredMaxNumberOfConnectors
789}
fba11dc6
JB
790
791const checkConfiguredMaxConnectors = (
792 configuredMaxConnectors: number,
793 logPrefix: string,
66a7748d 794 templateFile: string
fba11dc6
JB
795): void => {
796 if (configuredMaxConnectors <= 0) {
797 logger.warn(
66a7748d
JB
798 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
799 )
cda5d0fb 800 }
66a7748d 801}
cda5d0fb 802
fba11dc6
JB
803const checkTemplateMaxConnectors = (
804 templateMaxConnectors: number,
805 logPrefix: string,
66a7748d 806 templateFile: string
fba11dc6
JB
807): void => {
808 if (templateMaxConnectors === 0) {
809 logger.warn(
66a7748d
JB
810 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
811 )
fba11dc6
JB
812 } else if (templateMaxConnectors < 0) {
813 logger.error(
66a7748d
JB
814 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
815 )
fba11dc6 816 }
66a7748d 817}
fba11dc6
JB
818
819const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
66a7748d
JB
820 connectorStatus.availability = AvailabilityType.Operative
821 connectorStatus.idTagLocalAuthorized = false
822 connectorStatus.idTagAuthorized = false
823 connectorStatus.transactionRemoteStarted = false
824 connectorStatus.transactionStarted = false
825 connectorStatus.energyActiveImportRegisterValue = 0
826 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
300418e9 827 if (connectorStatus.chargingProfiles == null) {
66a7748d 828 connectorStatus.chargingProfiles = []
fba11dc6 829 }
66a7748d 830}
fba11dc6
JB
831
832const warnDeprecatedTemplateKey = (
833 template: ChargingStationTemplate,
834 key: string,
835 logPrefix: string,
836 templateFile: string,
66a7748d 837 logMsgToAppend = ''
fba11dc6 838): void => {
d760a0a6 839 if (template[key as keyof ChargingStationTemplate] != null) {
fba11dc6
JB
840 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
841 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
66a7748d
JB
842 }`
843 logger.warn(`${logPrefix} ${logMsg}`)
844 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
fba11dc6 845 }
66a7748d 846}
fba11dc6
JB
847
848const convertDeprecatedTemplateKey = (
849 template: ChargingStationTemplate,
850 deprecatedKey: string,
66a7748d 851 key?: string
fba11dc6 852): void => {
d760a0a6
JB
853 if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
854 if (key != null) {
300418e9 855 (template as unknown as Record<string, unknown>)[key] =
66a7748d 856 template[deprecatedKey as keyof ChargingStationTemplate]
e1d9a0f4 857 }
66a7748d
JB
858 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
859 delete template[deprecatedKey as keyof ChargingStationTemplate]
fba11dc6 860 }
66a7748d 861}
fba11dc6 862
947f048a 863interface ChargingProfilesLimit {
66a7748d
JB
864 limit: number
865 chargingProfile: ChargingProfile
947f048a
JB
866}
867
fba11dc6 868/**
21ee4dc2 869 * Charging profiles shall already be sorted by connector id descending then stack level descending
fba11dc6 870 *
d467756c
JB
871 * @param chargingStation -
872 * @param connectorId -
fba11dc6
JB
873 * @param chargingProfiles -
874 * @param logPrefix -
947f048a 875 * @returns ChargingProfilesLimit
fba11dc6
JB
876 */
877const getLimitFromChargingProfiles = (
a71d4e70
JB
878 chargingStation: ChargingStation,
879 connectorId: number,
fba11dc6 880 chargingProfiles: ChargingProfile[],
66a7748d 881 logPrefix: string
947f048a 882): ChargingProfilesLimit | undefined => {
66a7748d
JB
883 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
884 const currentDate = new Date()
f938317f 885 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
7abb61bb 886 let previousActiveChargingProfile: ChargingProfile | undefined
fba11dc6 887 for (const chargingProfile of chargingProfiles) {
66a7748d 888 const chargingSchedule = chargingProfile.chargingSchedule
2466918c 889 if (chargingSchedule.startSchedule == null) {
109c677a 890 logger.debug(
66a7748d
JB
891 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
892 )
a71d4e70 893 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
f938317f 894 chargingSchedule.startSchedule = connectorStatus?.transactionStart
52952bf8 895 }
2466918c 896 if (!isDate(chargingSchedule.startSchedule)) {
ef9e3b33 897 logger.warn(
66a7748d
JB
898 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
899 )
900 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
5199f9fd 901 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
ef9e3b33 902 }
2466918c 903 if (chargingSchedule.duration == null) {
da332e70 904 logger.debug(
66a7748d
JB
905 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
906 )
da332e70 907 // OCPP specifies that if duration is not defined, it should be infinite
be9f397b 908 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
da332e70 909 }
0eb666db 910 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
66a7748d 911 continue
ec4a242a 912 }
0bd926c1 913 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
66a7748d 914 continue
142a66c9 915 }
fba11dc6
JB
916 // Check if the charging profile is active
917 if (
975e18ec 918 isWithinInterval(currentDate, {
2466918c
JB
919 start: chargingSchedule.startSchedule,
920 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
975e18ec 921 })
fba11dc6 922 ) {
252a7d22 923 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
924 const chargingSchedulePeriodCompareFn = (
925 a: ChargingSchedulePeriod,
66a7748d
JB
926 b: ChargingSchedulePeriod
927 ): number => a.startPeriod - b.startPeriod
80c58041 928 if (
6fc0c6f3 929 !isArraySorted<ChargingSchedulePeriod>(
80c58041 930 chargingSchedule.chargingSchedulePeriod,
66a7748d 931 chargingSchedulePeriodCompareFn
6fc0c6f3 932 )
80c58041
JB
933 ) {
934 logger.warn(
66a7748d
JB
935 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
936 )
937 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
80c58041 938 }
da332e70 939 // Check if the first schedule period startPeriod property is equal to 0
55f2ab60
JB
940 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
941 logger.error(
66a7748d
JB
942 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
943 )
944 continue
55f2ab60 945 }
991fb26b 946 // Handle only one schedule period
975e18ec 947 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
948 const result: ChargingProfilesLimit = {
949 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
66a7748d
JB
950 chargingProfile
951 }
952 logger.debug(debugLogMsg, result)
953 return result
41189456 954 }
66a7748d 955 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
252a7d22 956 // Search for the right schedule period
e3037969
JB
957 for (const [
958 index,
66a7748d 959 chargingSchedulePeriod
e3037969 960 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
961 // Find the right schedule period
962 if (
963 isAfter(
2466918c 964 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
66a7748d 965 currentDate
252a7d22
JB
966 )
967 ) {
e3037969 968 // Found the schedule period: previous is the correct one
252a7d22 969 const result: ChargingProfilesLimit = {
7abb61bb
JB
970 limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
971 chargingProfile: previousActiveChargingProfile ?? chargingProfile
66a7748d
JB
972 }
973 logger.debug(debugLogMsg, result)
974 return result
252a7d22 975 }
975e18ec 976 // Handle the last schedule period within the charging profile duration
252a7d22 977 if (
975e18ec
JB
978 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
979 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
ccfa30bc
JB
980 differenceInSeconds(
981 addSeconds(
2466918c 982 chargingSchedule.startSchedule,
66a7748d 983 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
ccfa30bc 984 ),
2466918c
JB
985 chargingSchedule.startSchedule
986 ) > chargingSchedule.duration)
252a7d22
JB
987 ) {
988 const result: ChargingProfilesLimit = {
7abb61bb 989 limit: chargingSchedulePeriod.limit,
66a7748d
JB
990 chargingProfile
991 }
992 logger.debug(debugLogMsg, result)
993 return result
252a7d22 994 }
7abb61bb
JB
995 // Keep a reference to previous charging schedule period
996 previousChargingSchedulePeriod = chargingSchedulePeriod
17ac262c
JB
997 }
998 }
7abb61bb
JB
999 // Keep a reference to previous active charging profile
1000 previousActiveChargingProfile = chargingProfile
17ac262c 1001 }
17ac262c 1002 }
66a7748d 1003}
17ac262c 1004
0eb666db 1005export const prepareChargingProfileKind = (
f938317f 1006 connectorStatus: ConnectorStatus | undefined,
0eb666db 1007 chargingProfile: ChargingProfile,
6dde6c5f 1008 currentDate: string | number | Date,
66a7748d 1009 logPrefix: string
0eb666db
JB
1010): boolean => {
1011 switch (chargingProfile.chargingProfileKind) {
1012 case ChargingProfileKindType.RECURRING:
1013 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
66a7748d 1014 return false
0eb666db 1015 }
66a7748d
JB
1016 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
1017 break
0eb666db 1018 case ChargingProfileKindType.RELATIVE:
be9f397b 1019 if (chargingProfile.chargingSchedule.startSchedule != null) {
ccfa30bc 1020 logger.warn(
66a7748d
JB
1021 `${logPrefix} ${moduleName}.prepareChargingProfileKind: Relative charging profile id ${chargingProfile.chargingProfileId} has a startSchedule property defined. It will be ignored or used if the connector has a transaction started`
1022 )
1023 delete chargingProfile.chargingSchedule.startSchedule
ccfa30bc 1024 }
f938317f 1025 if (connectorStatus?.transactionStarted === true) {
5199f9fd 1026 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
ef9e3b33 1027 }
24dc52e9 1028 // FIXME: handle relative charging profile duration
66a7748d 1029 break
0eb666db 1030 }
66a7748d
JB
1031 return true
1032}
0eb666db 1033
ad490d5f 1034export const canProceedChargingProfile = (
0bd926c1 1035 chargingProfile: ChargingProfile,
6dde6c5f 1036 currentDate: string | number | Date,
66a7748d 1037 logPrefix: string
0bd926c1
JB
1038): boolean => {
1039 if (
5dc7c990
JB
1040 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
1041 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
0bd926c1
JB
1042 ) {
1043 logger.debug(
1044 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1045 chargingProfile.chargingProfileId
6dde6c5f 1046 } is not valid for the current date ${
5dc7c990 1047 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 1048 }`
66a7748d
JB
1049 )
1050 return false
0bd926c1 1051 }
ef9e3b33 1052 if (
be9f397b
JB
1053 chargingProfile.chargingSchedule.startSchedule == null ||
1054 chargingProfile.chargingSchedule.duration == null
ef9e3b33 1055 ) {
0bd926c1 1056 logger.error(
66a7748d
JB
1057 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1058 )
1059 return false
ef9e3b33 1060 }
5dc7c990 1061 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
ef9e3b33 1062 logger.error(
66a7748d
JB
1063 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1064 )
1065 return false
ef9e3b33 1066 }
5199f9fd 1067 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
ef9e3b33 1068 logger.error(
66a7748d
JB
1069 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1070 )
1071 return false
0bd926c1 1072 }
66a7748d
JB
1073 return true
1074}
0bd926c1 1075
0eb666db 1076const canProceedRecurringChargingProfile = (
0bd926c1 1077 chargingProfile: ChargingProfile,
66a7748d 1078 logPrefix: string
0bd926c1
JB
1079): boolean => {
1080 if (
1081 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1082 chargingProfile.recurrencyKind == null
0bd926c1
JB
1083 ) {
1084 logger.error(
66a7748d
JB
1085 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1086 )
1087 return false
0bd926c1 1088 }
d929adcc
JB
1089 if (
1090 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
be9f397b 1091 chargingProfile.chargingSchedule.startSchedule == null
d929adcc 1092 ) {
ef9e3b33 1093 logger.error(
66a7748d
JB
1094 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1095 )
1096 return false
ef9e3b33 1097 }
66a7748d
JB
1098 return true
1099}
0bd926c1 1100
522e4b05 1101/**
ec4a242a 1102 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
1103 *
1104 * @param chargingProfile -
1105 * @param currentDate -
1106 * @param logPrefix -
1107 */
0eb666db 1108const prepareRecurringChargingProfile = (
76dab5a9 1109 chargingProfile: ChargingProfile,
6dde6c5f 1110 currentDate: string | number | Date,
66a7748d 1111 logPrefix: string
ec4a242a 1112): boolean => {
66a7748d
JB
1113 const chargingSchedule = chargingProfile.chargingSchedule
1114 let recurringIntervalTranslated = false
5dc7c990 1115 let recurringInterval: Interval | undefined
76dab5a9
JB
1116 switch (chargingProfile.recurrencyKind) {
1117 case RecurrencyKindType.DAILY:
522e4b05 1118 recurringInterval = {
66a7748d 1119 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1120 start: chargingSchedule.startSchedule!,
66a7748d
JB
1121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1122 end: addDays(chargingSchedule.startSchedule!, 1)
1123 }
1124 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1125 if (
1126 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1127 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1128 ) {
1129 chargingSchedule.startSchedule = addDays(
991fb26b 1130 recurringInterval.start,
66a7748d
JB
1131 differenceInDays(currentDate, recurringInterval.start)
1132 )
522e4b05
JB
1133 recurringInterval = {
1134 start: chargingSchedule.startSchedule,
66a7748d
JB
1135 end: addDays(chargingSchedule.startSchedule, 1)
1136 }
1137 recurringIntervalTranslated = true
76dab5a9 1138 }
66a7748d 1139 break
76dab5a9 1140 case RecurrencyKindType.WEEKLY:
522e4b05 1141 recurringInterval = {
66a7748d 1142 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
522e4b05 1143 start: chargingSchedule.startSchedule!,
66a7748d
JB
1144 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1145 end: addWeeks(chargingSchedule.startSchedule!, 1)
1146 }
1147 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
522e4b05
JB
1148 if (
1149 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 1150 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
1151 ) {
1152 chargingSchedule.startSchedule = addWeeks(
991fb26b 1153 recurringInterval.start,
66a7748d
JB
1154 differenceInWeeks(currentDate, recurringInterval.start)
1155 )
522e4b05
JB
1156 recurringInterval = {
1157 start: chargingSchedule.startSchedule,
66a7748d
JB
1158 end: addWeeks(chargingSchedule.startSchedule, 1)
1159 }
1160 recurringIntervalTranslated = true
76dab5a9 1161 }
66a7748d 1162 break
ec4a242a
JB
1163 default:
1164 logger.error(
66a7748d
JB
1165 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1166 )
76dab5a9 1167 }
66a7748d 1168 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ec4a242a 1169 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 1170 logger.error(
aa5c5ad4 1171 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 1172 chargingProfile.recurrencyKind
991fb26b 1173 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
5dc7c990 1174 recurringInterval?.start as Date
991fb26b 1175 ).toISOString()}, ${toDate(
5dc7c990 1176 recurringInterval?.end as Date
6dde6c5f 1177 ).toISOString()}] has not been properly translated to current date ${
5dc7c990 1178 isDate(currentDate) ? currentDate.toISOString() : currentDate
6dde6c5f 1179 } `
66a7748d 1180 )
522e4b05 1181 }
66a7748d
JB
1182 return recurringIntervalTranslated
1183}
76dab5a9 1184
d476bc1b
JB
1185const checkRecurringChargingProfileDuration = (
1186 chargingProfile: ChargingProfile,
1187 interval: Interval,
66a7748d 1188 logPrefix: string
ec4a242a 1189): void => {
be9f397b 1190 if (chargingProfile.chargingSchedule.duration == null) {
142a66c9
JB
1191 logger.warn(
1192 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1193 chargingProfile.chargingProfileKind
1194 } charging profile id ${
1195 chargingProfile.chargingProfileId
1196 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1197 interval.end,
66a7748d
JB
1198 interval.start
1199 )}`
1200 )
1201 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
142a66c9 1202 } else if (
be9f397b 1203 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
d476bc1b
JB
1204 ) {
1205 logger.warn(
aa5c5ad4 1206 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1207 chargingProfile.chargingProfileKind
1208 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1209 chargingProfile.chargingSchedule.duration
710d50eb 1210 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b 1211 interval.end,
66a7748d
JB
1212 interval.start
1213 )}`
1214 )
1215 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
d476bc1b 1216 }
66a7748d 1217}
d476bc1b 1218
fba11dc6 1219const getRandomSerialNumberSuffix = (params?: {
66a7748d
JB
1220 randomBytesLength?: number
1221 upperCase?: boolean
fba11dc6 1222}): string => {
66a7748d
JB
1223 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1224 if (params?.upperCase === true) {
1225 return randomSerialNumberSuffix.toUpperCase()
17ac262c 1226 }
66a7748d
JB
1227 return randomSerialNumberSuffix
1228}