1 import { createHash
, randomBytes
} from
'node:crypto'
2 import type { EventEmitter
} from
'node:events'
3 import { basename
, dirname
, isAbsolute
, join
, parse
, relative
, resolve
} from
'node:path'
4 import { env
} from
'node:process'
5 import { fileURLToPath
} from
'node:url'
7 import chalk from
'chalk'
23 import { maxTime
} from
'date-fns/constants'
25 import { BaseError
} from
'../exception/index.js'
29 type BootNotificationRequest
,
32 ChargingProfileKindType
,
34 type ChargingSchedulePeriod
,
35 type ChargingStationConfiguration
,
36 type ChargingStationInfo
,
37 type ChargingStationOptions
,
38 type ChargingStationTemplate
,
39 type ChargingStationWorkerMessageEvents
,
40 ConnectorPhaseRotation
,
45 type OCPP16BootNotificationRequest
,
46 type OCPP20BootNotificationRequest
,
50 ReservationTerminationReason
,
51 StandardParametersKey
,
52 type SupportedFeatureProfiles
,
54 } from
'../types/index.js'
70 } from
'../utils/index.js'
71 import type { ChargingStation
} from
'./ChargingStation.js'
72 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
74 const moduleName
= 'Helpers'
76 export const buildTemplateName
= (templateFile
: string): string => {
77 if (isAbsolute(templateFile
)) {
78 templateFile
= relative(
79 resolve(join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'station-templates')),
83 const templateFileParsedPath
= parse(templateFile
)
84 return join(templateFileParsedPath
.dir
, templateFileParsedPath
.name
)
87 export const getChargingStationId
= (
89 stationTemplate
: ChargingStationTemplate
| undefined
91 if (stationTemplate
== null) {
92 return "Unknown 'chargingStationId'"
94 // In case of multiple instances: add instance index to charging station id
95 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
96 const idSuffix
= stationTemplate
.nameSuffix
?? ''
97 const idStr
= `000000000${index.toString()}`
98 return stationTemplate
.fixedName
=== true
99 ? stationTemplate
.baseName
100 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
105 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
106 return isPast(reservation
.expiryDate
)
109 export const removeExpiredReservations
= async (
110 chargingStation
: ChargingStation
111 ): Promise
<void> => {
112 if (chargingStation
.hasEvses
) {
113 for (const evseStatus
of chargingStation
.evses
.values()) {
114 for (const connectorStatus
of evseStatus
.connectors
.values()) {
116 connectorStatus
.reservation
!= null &&
117 hasReservationExpired(connectorStatus
.reservation
)
119 await chargingStation
.removeReservation(
120 connectorStatus
.reservation
,
121 ReservationTerminationReason
.EXPIRED
127 for (const connectorStatus
of chargingStation
.connectors
.values()) {
129 connectorStatus
.reservation
!= null &&
130 hasReservationExpired(connectorStatus
.reservation
)
132 await chargingStation
.removeReservation(
133 connectorStatus
.reservation
,
134 ReservationTerminationReason
.EXPIRED
141 export const getNumberOfReservableConnectors
= (
142 connectors
: Map
<number, ConnectorStatus
>
144 let numberOfReservableConnectors
= 0
145 for (const [connectorId
, connectorStatus
] of connectors
) {
146 if (connectorId
=== 0) {
149 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
150 ++numberOfReservableConnectors
153 return numberOfReservableConnectors
156 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
157 const chargingStationInfo
= {
158 chargePointModel
: stationTemplate
.chargePointModel
,
159 chargePointVendor
: stationTemplate
.chargePointVendor
,
160 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!= null && {
161 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
163 ...(stationTemplate
.chargePointSerialNumberPrefix
!= null && {
164 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
166 ...(stationTemplate
.meterSerialNumberPrefix
!= null && {
167 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
169 ...(stationTemplate
.meterType
!= null && {
170 meterType
: stationTemplate
.meterType
173 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
174 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
178 export const checkChargingStation
= (
179 chargingStation
: ChargingStation
,
182 if (!chargingStation
.started
&& !chargingStation
.starting
) {
183 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
189 export const getPhaseRotationValue
= (
191 numberOfPhases
: number
192 ): string | undefined => {
194 if (connectorId
=== 0 && numberOfPhases
=== 0) {
195 return `${connectorId}.${ConnectorPhaseRotation.RST}`
196 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
197 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
199 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
200 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
201 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
202 return `${connectorId}.${ConnectorPhaseRotation.RST}`
206 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
210 return Object.keys(evses
).length
213 const getMaxNumberOfConnectors
= (
214 connectors
: Record
<string, ConnectorStatus
> | undefined
216 if (connectors
== null) {
219 return Object.keys(connectors
).length
222 export const getBootConnectorStatus
= (
223 chargingStation
: ChargingStation
,
225 connectorStatus
: ConnectorStatus
226 ): ConnectorStatusEnum
=> {
227 let connectorBootStatus
: ConnectorStatusEnum
229 connectorStatus
.status == null &&
230 (!chargingStation
.isChargingStationAvailable() ||
231 !chargingStation
.isConnectorAvailable(connectorId
))
233 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
234 } else if (connectorStatus
.status == null && connectorStatus
.bootStatus
!= null) {
235 // Set boot status in template at startup
236 connectorBootStatus
= connectorStatus
.bootStatus
237 } else if (connectorStatus
.status != null) {
238 // Set previous status at startup
239 connectorBootStatus
= connectorStatus
.status
241 // Set default status
242 connectorBootStatus
= ConnectorStatusEnum
.Available
244 return connectorBootStatus
247 export const checkTemplate
= (
248 stationTemplate
: ChargingStationTemplate
| undefined,
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
)
257 if (isEmptyObject(stationTemplate
)) {
258 const errorMsg
= `Empty charging station information from template file ${templateFile}`
259 logger
.error(`${logPrefix} ${errorMsg}`)
260 throw new BaseError(errorMsg
)
262 if (stationTemplate
.idTagsFile
== null || isEmptyString(stationTemplate
.idTagsFile
)) {
264 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
269 export const checkConfiguration
= (
270 stationConfiguration
: ChargingStationConfiguration
| undefined,
272 configurationFile
: string
274 if (stationConfiguration
== null) {
275 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
276 logger
.error(`${logPrefix} ${errorMsg}`)
277 throw new BaseError(errorMsg
)
279 if (isEmptyObject(stationConfiguration
)) {
280 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
281 logger
.error(`${logPrefix} ${errorMsg}`)
282 throw new BaseError(errorMsg
)
286 export const checkConnectorsConfiguration
= (
287 stationTemplate
: ChargingStationTemplate
,
291 configuredMaxConnectors
: number
292 templateMaxConnectors
: number
293 templateMaxAvailableConnectors
: number
295 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
296 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
297 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
298 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
299 const templateMaxAvailableConnectors
=
300 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
302 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
303 stationTemplate
.randomConnectors
!== true
306 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
308 stationTemplate
.randomConnectors
= true
310 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
}
313 export const checkStationInfoConnectorStatus
= (
315 connectorStatus
: ConnectorStatus
,
319 if (connectorStatus
.status != null) {
321 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
323 delete connectorStatus
.status
327 export const buildConnectorsMap
= (
328 connectors
: Record
<string, ConnectorStatus
>,
331 ): Map
<number, ConnectorStatus
> => {
332 const connectorsMap
= new Map
<number, ConnectorStatus
>()
333 if (getMaxNumberOfConnectors(connectors
) > 0) {
334 for (const connector
in connectors
) {
335 const connectorStatus
= connectors
[connector
]
336 const connectorId
= convertToInt(connector
)
337 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
338 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
342 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
348 export const setChargingStationOptions
= (
349 stationInfo
: ChargingStationInfo
,
350 options
?: ChargingStationOptions
351 ): ChargingStationInfo
=> {
352 if (options
?.supervisionUrls
!= null) {
353 stationInfo
.supervisionUrls
= options
.supervisionUrls
355 if (options
?.persistentConfiguration
!= null) {
356 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
357 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
358 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
359 options
.persistentConfiguration
361 if (options
?.autoStart
!= null) {
362 stationInfo
.autoStart
= options
.autoStart
364 if (options
?.autoRegister
!= null) {
365 stationInfo
.autoRegister
= options
.autoRegister
367 if (options
?.enableStatistics
!= null) {
368 stationInfo
.enableStatistics
= options
.enableStatistics
370 if (options
?.ocppStrictCompliance
!= null) {
371 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
373 if (options
?.stopTransactionsOnStopped
!= null) {
374 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
379 export const initializeConnectorsMapStatus
= (
380 connectors
: Map
<number, ConnectorStatus
>,
383 for (const connectorId
of connectors
.keys()) {
384 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
386 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
387 connectors.get(connectorId)?.transactionId
391 if (connectorId
=== 0) {
392 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
393 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
394 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
395 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
396 connectors
.get(connectorId
)!.chargingProfiles
= []
398 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 initializeConnectorStatus(connectors
.get(connectorId
)!)
405 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
406 if (connectorStatus
== null) {
409 connectorStatus
.chargingProfiles
=
410 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
411 ? connectorStatus
.chargingProfiles
.filter(
412 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
415 connectorStatus
.idTagLocalAuthorized
= false
416 connectorStatus
.idTagAuthorized
= false
417 connectorStatus
.transactionRemoteStarted
= false
418 connectorStatus
.transactionStarted
= false
419 delete connectorStatus
.transactionStart
420 delete connectorStatus
.transactionId
421 delete connectorStatus
.localAuthorizeIdTag
422 delete connectorStatus
.authorizeIdTag
423 delete connectorStatus
.transactionIdTag
424 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
425 delete connectorStatus
.transactionBeginMeterValue
428 export const createBootNotificationRequest
= (
429 stationInfo
: ChargingStationInfo
,
430 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
431 ): BootNotificationRequest
| undefined => {
432 const ocppVersion
= stationInfo
.ocppVersion
433 switch (ocppVersion
) {
434 case OCPPVersion
.VERSION_16
:
436 chargePointModel
: stationInfo
.chargePointModel
,
437 chargePointVendor
: stationInfo
.chargePointVendor
,
438 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
439 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
441 ...(stationInfo
.chargePointSerialNumber
!= null && {
442 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
444 ...(stationInfo
.firmwareVersion
!= null && {
445 firmwareVersion
: stationInfo
.firmwareVersion
447 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
448 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
449 ...(stationInfo
.meterSerialNumber
!= null && {
450 meterSerialNumber
: stationInfo
.meterSerialNumber
452 ...(stationInfo
.meterType
!= null && {
453 meterType
: stationInfo
.meterType
455 } satisfies OCPP16BootNotificationRequest
456 case OCPPVersion
.VERSION_20
:
457 case OCPPVersion
.VERSION_201
:
461 model
: stationInfo
.chargePointModel
,
462 vendorName
: stationInfo
.chargePointVendor
,
463 ...(stationInfo
.firmwareVersion
!= null && {
464 firmwareVersion
: stationInfo
.firmwareVersion
466 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
467 serialNumber
: stationInfo
.chargeBoxSerialNumber
469 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
471 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
472 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
476 } satisfies OCPP20BootNotificationRequest
480 export const warnTemplateKeysDeprecation
= (
481 stationTemplate
: ChargingStationTemplate
,
485 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
486 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
487 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
488 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
489 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
491 for (const templateKey
of templateKeys
) {
492 warnDeprecatedTemplateKey(
494 templateKey
.deprecatedKey
,
497 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
499 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
503 export const stationTemplateToStationInfo
= (
504 stationTemplate
: ChargingStationTemplate
505 ): ChargingStationInfo
=> {
506 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
507 delete stationTemplate
.power
508 delete stationTemplate
.powerUnit
509 delete stationTemplate
.Connectors
510 delete stationTemplate
.Evses
511 delete stationTemplate
.Configuration
512 delete stationTemplate
.AutomaticTransactionGenerator
513 delete stationTemplate
.numberOfConnectors
514 delete stationTemplate
.chargeBoxSerialNumberPrefix
515 delete stationTemplate
.chargePointSerialNumberPrefix
516 delete stationTemplate
.meterSerialNumberPrefix
517 return stationTemplate
as ChargingStationInfo
520 export const createSerialNumber
= (
521 stationTemplate
: ChargingStationTemplate
,
522 stationInfo
: ChargingStationInfo
,
524 randomSerialNumberUpperCase
?: boolean
525 randomSerialNumber
?: boolean
528 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
529 const serialNumberSuffix
=
530 params
.randomSerialNumber
=== true
531 ? getRandomSerialNumberSuffix({
532 upperCase
: params
.randomSerialNumberUpperCase
535 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
536 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
537 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
538 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
539 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
540 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
543 export const propagateSerialNumber
= (
544 stationTemplate
: ChargingStationTemplate
| undefined,
545 stationInfoSrc
: ChargingStationInfo
| undefined,
546 stationInfoDst
: ChargingStationInfo
548 if (stationInfoSrc
== null || stationTemplate
== null) {
550 'Missing charging station template or existing configuration to propagate serial number'
553 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
554 stationInfoSrc
.chargePointSerialNumber
!= null
555 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
556 : stationInfoDst
.chargePointSerialNumber
!= null &&
557 delete stationInfoDst
.chargePointSerialNumber
558 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
559 stationInfoSrc
.chargeBoxSerialNumber
!= null
560 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
561 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
562 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
563 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
564 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
567 export const hasFeatureProfile
= (
568 chargingStation
: ChargingStation
,
569 featureProfile
: SupportedFeatureProfiles
570 ): boolean | undefined => {
571 return getConfigurationKey(
573 StandardParametersKey
.SupportedFeatureProfiles
574 )?.value
?.includes(featureProfile
)
577 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
579 switch (stationInfo
.amperageLimitationUnit
) {
580 case AmpereUnits
.DECI_AMPERE
:
583 case AmpereUnits
.CENTI_AMPERE
:
586 case AmpereUnits
.MILLI_AMPERE
:
594 * Gets the connector cloned charging profiles applying a power limitation
595 * and sorted by connector id descending then stack level descending
597 * @param chargingStation -
598 * @param connectorId -
599 * @returns connector charging profiles array
601 export const getConnectorChargingProfiles
= (
602 chargingStation
: ChargingStation
,
604 ): ChargingProfile
[] => {
605 return clone
<ChargingProfile
[]>(
606 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
607 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
609 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
610 (a
, b
) => b
.stackLevel
- a
.stackLevel
616 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
617 chargingStation
: ChargingStation
,
619 ): number | undefined => {
620 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
621 // Get charging profiles sorted by connector id then stack level
622 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
623 if (isNotEmptyArray(chargingProfiles
)) {
624 const result
= getLimitFromChargingProfiles(
628 chargingStation
.logPrefix()
630 if (result
!= null) {
632 chargingProfile
= result
.chargingProfile
633 switch (chargingStation
.stationInfo
?.currentOutType
) {
636 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
638 : ACElectricUtils
.powerTotal(
639 chargingStation
.getNumberOfPhases(),
640 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
641 chargingStation
.stationInfo
.voltageOut
!,
647 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
649 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
650 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
652 const connectorMaximumPower
=
653 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
654 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
655 if (limit
> connectorMaximumPower
) {
657 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
658 chargingProfile.chargingProfileId
659 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
662 limit
= connectorMaximumPower
669 export const getDefaultVoltageOut
= (
670 currentType
: CurrentType
,
674 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
675 let defaultVoltageOut
: number
676 switch (currentType
) {
678 defaultVoltageOut
= Voltage
.VOLTAGE_230
681 defaultVoltageOut
= Voltage
.VOLTAGE_400
684 logger
.error(`${logPrefix} ${errorMsg}`)
685 throw new BaseError(errorMsg
)
687 return defaultVoltageOut
690 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
691 return stationInfo
.idTagsFile
!= null
692 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
696 export const waitChargingStationEvents
= async (
697 emitter
: EventEmitter
,
698 event
: ChargingStationWorkerMessageEvents
,
700 ): Promise
<number> => {
701 return await new Promise
<number>(resolve
=> {
703 if (eventsToWait
=== 0) {
707 emitter
.on(event
, () => {
709 if (events
=== eventsToWait
) {
716 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
717 let configuredMaxNumberOfConnectors
= 0
718 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
719 const numberOfConnectors
= stationTemplate
.numberOfConnectors
720 configuredMaxNumberOfConnectors
=
721 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
722 } else if (stationTemplate
.numberOfConnectors
!= null) {
723 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
724 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
725 configuredMaxNumberOfConnectors
=
726 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
727 stationTemplate
.Connectors
[0] != null
728 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
729 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
730 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
731 for (const evse
in stationTemplate
.Evses
) {
735 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
736 stationTemplate
.Evses
[evse
].Connectors
740 return configuredMaxNumberOfConnectors
743 const checkConfiguredMaxConnectors
= (
744 configuredMaxConnectors
: number,
748 if (configuredMaxConnectors
<= 0) {
750 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
755 const checkTemplateMaxConnectors
= (
756 templateMaxConnectors
: number,
760 if (templateMaxConnectors
=== 0) {
762 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
764 } else if (templateMaxConnectors
< 0) {
766 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
771 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
772 connectorStatus
.availability
= AvailabilityType
.Operative
773 connectorStatus
.idTagLocalAuthorized
= false
774 connectorStatus
.idTagAuthorized
= false
775 connectorStatus
.transactionRemoteStarted
= false
776 connectorStatus
.transactionStarted
= false
777 connectorStatus
.energyActiveImportRegisterValue
= 0
778 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
779 if (connectorStatus
.chargingProfiles
== null) {
780 connectorStatus
.chargingProfiles
= []
784 const warnDeprecatedTemplateKey
= (
785 template
: ChargingStationTemplate
,
788 templateFile
: string,
791 if (template
[key
as keyof ChargingStationTemplate
] != null) {
792 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
793 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
795 logger
.warn(`${logPrefix} ${logMsg}`)
796 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
800 const convertDeprecatedTemplateKey
= (
801 template
: ChargingStationTemplate
,
802 deprecatedKey
: string,
805 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
807 (template
as unknown
as Record
<string, unknown
>)[key
] =
808 template
[deprecatedKey
as keyof ChargingStationTemplate
]
810 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
811 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
815 interface ChargingProfilesLimit
{
817 chargingProfile
: ChargingProfile
821 * Charging profiles shall already be sorted by connector id descending then stack level descending
823 * @param chargingStation -
824 * @param connectorId -
825 * @param chargingProfiles -
827 * @returns ChargingProfilesLimit
829 const getLimitFromChargingProfiles
= (
830 chargingStation
: ChargingStation
,
832 chargingProfiles
: ChargingProfile
[],
834 ): ChargingProfilesLimit
| undefined => {
835 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
836 const currentDate
= new Date()
837 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
838 for (const chargingProfile
of chargingProfiles
) {
839 const chargingSchedule
= chargingProfile
.chargingSchedule
840 if (chargingSchedule
.startSchedule
== null) {
842 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
844 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
845 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
847 if (!isDate(chargingSchedule
.startSchedule
)) {
849 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
851 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
852 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
854 if (chargingSchedule
.duration
== null) {
856 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
858 // OCPP specifies that if duration is not defined, it should be infinite
859 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
861 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
864 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
867 // Check if the charging profile is active
869 isWithinInterval(currentDate
, {
870 start
: chargingSchedule
.startSchedule
,
871 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
874 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
875 const chargingSchedulePeriodCompareFn
= (
876 a
: ChargingSchedulePeriod
,
877 b
: ChargingSchedulePeriod
878 ): number => a
.startPeriod
- b
.startPeriod
880 !isArraySorted
<ChargingSchedulePeriod
>(
881 chargingSchedule
.chargingSchedulePeriod
,
882 chargingSchedulePeriodCompareFn
886 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
888 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
890 // Check if the first schedule period startPeriod property is equal to 0
891 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
893 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
897 // Handle only one schedule period
898 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
899 const result
: ChargingProfilesLimit
= {
900 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
903 logger
.debug(debugLogMsg
, result
)
906 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
907 // Search for the right schedule period
910 chargingSchedulePeriod
911 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
912 // Find the right schedule period
915 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
919 // Found the schedule period: previous is the correct one
920 const result
: ChargingProfilesLimit
= {
921 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
922 limit
: previousChargingSchedulePeriod
!.limit
,
925 logger
.debug(debugLogMsg
, result
)
928 // Keep a reference to previous one
929 previousChargingSchedulePeriod
= chargingSchedulePeriod
930 // Handle the last schedule period within the charging profile duration
932 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
933 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
936 chargingSchedule
.startSchedule
,
937 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
939 chargingSchedule
.startSchedule
940 ) > chargingSchedule
.duration
)
942 const result
: ChargingProfilesLimit
= {
943 limit
: previousChargingSchedulePeriod
.limit
,
946 logger
.debug(debugLogMsg
, result
)
955 export const prepareChargingProfileKind
= (
956 connectorStatus
: ConnectorStatus
| undefined,
957 chargingProfile
: ChargingProfile
,
958 currentDate
: string | number | Date,
961 switch (chargingProfile
.chargingProfileKind
) {
962 case ChargingProfileKindType
.RECURRING
:
963 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
966 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
968 case ChargingProfileKindType
.RELATIVE
:
969 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
971 `${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`
973 delete chargingProfile
.chargingSchedule
.startSchedule
975 if (connectorStatus
?.transactionStarted
=== true) {
976 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
978 // FIXME: Handle relative charging profile duration
984 export const canProceedChargingProfile
= (
985 chargingProfile
: ChargingProfile
,
986 currentDate
: string | number | Date,
990 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
991 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
994 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
995 chargingProfile.chargingProfileId
996 } is not valid for the current date ${
997 isDate(currentDate) ? currentDate.toISOString() : currentDate
1003 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1004 chargingProfile
.chargingSchedule
.duration
== null
1007 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1011 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1013 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1017 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1019 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1026 const canProceedRecurringChargingProfile
= (
1027 chargingProfile
: ChargingProfile
,
1031 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1032 chargingProfile
.recurrencyKind
== null
1035 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1040 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1041 chargingProfile
.chargingSchedule
.startSchedule
== null
1044 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1052 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1054 * @param chargingProfile -
1055 * @param currentDate -
1056 * @param logPrefix -
1058 const prepareRecurringChargingProfile
= (
1059 chargingProfile
: ChargingProfile
,
1060 currentDate
: string | number | Date,
1063 const chargingSchedule
= chargingProfile
.chargingSchedule
1064 let recurringIntervalTranslated
= false
1065 let recurringInterval
: Interval
| undefined
1066 switch (chargingProfile
.recurrencyKind
) {
1067 case RecurrencyKindType
.DAILY
:
1068 recurringInterval
= {
1069 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1070 start
: chargingSchedule
.startSchedule
!,
1071 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1072 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1074 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1076 !isWithinInterval(currentDate
, recurringInterval
) &&
1077 isBefore(recurringInterval
.end
, currentDate
)
1079 chargingSchedule
.startSchedule
= addDays(
1080 recurringInterval
.start
,
1081 differenceInDays(currentDate
, recurringInterval
.start
)
1083 recurringInterval
= {
1084 start
: chargingSchedule
.startSchedule
,
1085 end
: addDays(chargingSchedule
.startSchedule
, 1)
1087 recurringIntervalTranslated
= true
1090 case RecurrencyKindType
.WEEKLY
:
1091 recurringInterval
= {
1092 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1093 start
: chargingSchedule
.startSchedule
!,
1094 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1095 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1097 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1099 !isWithinInterval(currentDate
, recurringInterval
) &&
1100 isBefore(recurringInterval
.end
, currentDate
)
1102 chargingSchedule
.startSchedule
= addWeeks(
1103 recurringInterval
.start
,
1104 differenceInWeeks(currentDate
, recurringInterval
.start
)
1106 recurringInterval
= {
1107 start
: chargingSchedule
.startSchedule
,
1108 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1110 recurringIntervalTranslated
= true
1115 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1118 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1119 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1121 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1122 chargingProfile.recurrencyKind
1123 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1124 recurringInterval?.start as Date
1125 ).toISOString()}, ${toDate(
1126 recurringInterval?.end as Date
1127 ).toISOString()}] has not been properly translated to current date ${
1128 isDate(currentDate) ? currentDate.toISOString() : currentDate
1132 return recurringIntervalTranslated
1135 const checkRecurringChargingProfileDuration
= (
1136 chargingProfile
: ChargingProfile
,
1140 if (chargingProfile
.chargingSchedule
.duration
== null) {
1142 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1143 chargingProfile.chargingProfileKind
1144 } charging profile id ${
1145 chargingProfile.chargingProfileId
1146 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1151 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1153 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1156 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1157 chargingProfile.chargingProfileKind
1158 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1159 chargingProfile.chargingSchedule.duration
1160 } is greater than the recurrency time interval duration ${differenceInSeconds(
1165 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1169 const getRandomSerialNumberSuffix
= (params
?: {
1170 randomBytesLength
?: number
1173 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1174 if (params
?.upperCase
=== true) {
1175 return randomSerialNumberSuffix
.toUpperCase()
1177 return randomSerialNumberSuffix