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'
24 import { isEmpty
} from
'rambda'
26 import { BaseError
} from
'../exception/index.js'
30 type BootNotificationRequest
,
33 ChargingProfileKindType
,
35 type ChargingSchedulePeriod
,
36 type ChargingStationConfiguration
,
37 type ChargingStationInfo
,
38 type ChargingStationOptions
,
39 type ChargingStationTemplate
,
40 type ChargingStationWorkerMessageEvents
,
41 ConnectorPhaseRotation
,
46 type OCPP16BootNotificationRequest
,
47 type OCPP20BootNotificationRequest
,
51 ReservationTerminationReason
,
52 StandardParametersKey
,
53 type SupportedFeatureProfiles
,
55 } from
'../types/index.js'
69 } from
'../utils/index.js'
70 import type { ChargingStation
} from
'./ChargingStation.js'
71 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
73 const moduleName
= 'Helpers'
75 export const buildTemplateName
= (templateFile
: string): string => {
76 if (isAbsolute(templateFile
)) {
77 templateFile
= relative(
78 resolve(join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'station-templates')),
82 const templateFileParsedPath
= parse(templateFile
)
83 return join(templateFileParsedPath
.dir
, templateFileParsedPath
.name
)
86 export const getChargingStationId
= (
88 stationTemplate
: ChargingStationTemplate
| undefined
90 if (stationTemplate
== null) {
91 return "Unknown 'chargingStationId'"
93 // In case of multiple instances: add instance index to charging station id
94 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
95 const idSuffix
= stationTemplate
.nameSuffix
?? ''
96 const idStr
= `000000000${index.toString()}`
97 return stationTemplate
.fixedName
=== true
98 ? stationTemplate
.baseName
99 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
104 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
105 return isPast(reservation
.expiryDate
)
108 export const removeExpiredReservations
= async (
109 chargingStation
: ChargingStation
110 ): Promise
<void> => {
111 if (chargingStation
.hasEvses
) {
112 for (const evseStatus
of chargingStation
.evses
.values()) {
113 for (const connectorStatus
of evseStatus
.connectors
.values()) {
115 connectorStatus
.reservation
!= null &&
116 hasReservationExpired(connectorStatus
.reservation
)
118 await chargingStation
.removeReservation(
119 connectorStatus
.reservation
,
120 ReservationTerminationReason
.EXPIRED
126 for (const connectorStatus
of chargingStation
.connectors
.values()) {
128 connectorStatus
.reservation
!= null &&
129 hasReservationExpired(connectorStatus
.reservation
)
131 await chargingStation
.removeReservation(
132 connectorStatus
.reservation
,
133 ReservationTerminationReason
.EXPIRED
140 export const getNumberOfReservableConnectors
= (
141 connectors
: Map
<number, ConnectorStatus
>
143 let numberOfReservableConnectors
= 0
144 for (const [connectorId
, connectorStatus
] of connectors
) {
145 if (connectorId
=== 0) {
148 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
149 ++numberOfReservableConnectors
152 return numberOfReservableConnectors
155 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
156 const chargingStationInfo
= {
157 chargePointModel
: stationTemplate
.chargePointModel
,
158 chargePointVendor
: stationTemplate
.chargePointVendor
,
159 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!= null && {
160 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
162 ...(stationTemplate
.chargePointSerialNumberPrefix
!= null && {
163 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
165 ...(stationTemplate
.meterSerialNumberPrefix
!= null && {
166 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
168 ...(stationTemplate
.meterType
!= null && {
169 meterType
: stationTemplate
.meterType
172 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
173 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
177 export const checkChargingStation
= (
178 chargingStation
: ChargingStation
,
181 if (!chargingStation
.started
&& !chargingStation
.starting
) {
182 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
188 export const getPhaseRotationValue
= (
190 numberOfPhases
: number
191 ): string | undefined => {
193 if (connectorId
=== 0 && numberOfPhases
=== 0) {
194 return `${connectorId}.${ConnectorPhaseRotation.RST}`
195 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
196 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
198 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
199 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
200 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
201 return `${connectorId}.${ConnectorPhaseRotation.RST}`
205 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
209 return Object.keys(evses
).length
212 const getMaxNumberOfConnectors
= (
213 connectors
: Record
<string, ConnectorStatus
> | undefined
215 if (connectors
== null) {
218 return Object.keys(connectors
).length
221 export const getBootConnectorStatus
= (
222 chargingStation
: ChargingStation
,
224 connectorStatus
: ConnectorStatus
225 ): ConnectorStatusEnum
=> {
226 let connectorBootStatus
: ConnectorStatusEnum
228 connectorStatus
.status == null &&
229 (!chargingStation
.isChargingStationAvailable() ||
230 !chargingStation
.isConnectorAvailable(connectorId
))
232 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
233 } else if (connectorStatus
.status == null && connectorStatus
.bootStatus
!= null) {
234 // Set boot status in template at startup
235 connectorBootStatus
= connectorStatus
.bootStatus
236 } else if (connectorStatus
.status != null) {
237 // Set previous status at startup
238 connectorBootStatus
= connectorStatus
.status
240 // Set default status
241 connectorBootStatus
= ConnectorStatusEnum
.Available
243 return connectorBootStatus
246 export const checkTemplate
= (
247 stationTemplate
: ChargingStationTemplate
| undefined,
251 if (stationTemplate
== null) {
252 const errorMsg
= `Failed to read charging station template file ${templateFile}`
253 logger
.error(`${logPrefix} ${errorMsg}`)
254 throw new BaseError(errorMsg
)
256 if (isEmpty(stationTemplate
)) {
257 const errorMsg
= `Empty charging station information from template file ${templateFile}`
258 logger
.error(`${logPrefix} ${errorMsg}`)
259 throw new BaseError(errorMsg
)
261 if (stationTemplate
.idTagsFile
== null || isEmpty(stationTemplate
.idTagsFile
)) {
263 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
268 export const checkConfiguration
= (
269 stationConfiguration
: ChargingStationConfiguration
| undefined,
271 configurationFile
: string
273 if (stationConfiguration
== null) {
274 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
275 logger
.error(`${logPrefix} ${errorMsg}`)
276 throw new BaseError(errorMsg
)
278 if (isEmpty(stationConfiguration
)) {
279 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
280 logger
.error(`${logPrefix} ${errorMsg}`)
281 throw new BaseError(errorMsg
)
285 export const checkConnectorsConfiguration
= (
286 stationTemplate
: ChargingStationTemplate
,
290 configuredMaxConnectors
: number
291 templateMaxConnectors
: number
292 templateMaxAvailableConnectors
: number
294 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
295 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
296 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
297 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
298 const templateMaxAvailableConnectors
=
299 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
301 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
302 stationTemplate
.randomConnectors
!== true
305 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
307 stationTemplate
.randomConnectors
= true
309 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
}
312 export const checkStationInfoConnectorStatus
= (
314 connectorStatus
: ConnectorStatus
,
318 if (connectorStatus
.status != null) {
320 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
322 delete connectorStatus
.status
326 export const buildConnectorsMap
= (
327 connectors
: Record
<string, ConnectorStatus
>,
330 ): Map
<number, ConnectorStatus
> => {
331 const connectorsMap
= new Map
<number, ConnectorStatus
>()
332 if (getMaxNumberOfConnectors(connectors
) > 0) {
333 for (const connector
in connectors
) {
334 const connectorStatus
= connectors
[connector
]
335 const connectorId
= convertToInt(connector
)
336 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
337 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
341 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
347 export const setChargingStationOptions
= (
348 stationInfo
: ChargingStationInfo
,
349 options
?: ChargingStationOptions
350 ): ChargingStationInfo
=> {
351 if (options
?.supervisionUrls
!= null) {
352 stationInfo
.supervisionUrls
= options
.supervisionUrls
354 if (options
?.persistentConfiguration
!= null) {
355 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
356 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
357 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
358 options
.persistentConfiguration
360 if (options
?.autoStart
!= null) {
361 stationInfo
.autoStart
= options
.autoStart
363 if (options
?.autoRegister
!= null) {
364 stationInfo
.autoRegister
= options
.autoRegister
366 if (options
?.enableStatistics
!= null) {
367 stationInfo
.enableStatistics
= options
.enableStatistics
369 if (options
?.ocppStrictCompliance
!= null) {
370 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
372 if (options
?.stopTransactionsOnStopped
!= null) {
373 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
378 export const initializeConnectorsMapStatus
= (
379 connectors
: Map
<number, ConnectorStatus
>,
382 for (const connectorId
of connectors
.keys()) {
383 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
385 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
386 connectors.get(connectorId)?.transactionId
390 if (connectorId
=== 0) {
391 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
392 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
393 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
394 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
395 connectors
.get(connectorId
)!.chargingProfiles
= []
397 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
398 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
399 initializeConnectorStatus(connectors
.get(connectorId
)!)
404 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
405 if (connectorStatus
== null) {
408 connectorStatus
.chargingProfiles
=
409 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
410 ? connectorStatus
.chargingProfiles
.filter(
411 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
414 connectorStatus
.idTagLocalAuthorized
= false
415 connectorStatus
.idTagAuthorized
= false
416 connectorStatus
.transactionRemoteStarted
= false
417 connectorStatus
.transactionStarted
= false
418 delete connectorStatus
.transactionStart
419 delete connectorStatus
.transactionId
420 delete connectorStatus
.localAuthorizeIdTag
421 delete connectorStatus
.authorizeIdTag
422 delete connectorStatus
.transactionIdTag
423 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
424 delete connectorStatus
.transactionBeginMeterValue
427 export const createBootNotificationRequest
= (
428 stationInfo
: ChargingStationInfo
,
429 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
430 ): BootNotificationRequest
| undefined => {
431 const ocppVersion
= stationInfo
.ocppVersion
432 switch (ocppVersion
) {
433 case OCPPVersion
.VERSION_16
:
435 chargePointModel
: stationInfo
.chargePointModel
,
436 chargePointVendor
: stationInfo
.chargePointVendor
,
437 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
438 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
440 ...(stationInfo
.chargePointSerialNumber
!= null && {
441 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
443 ...(stationInfo
.firmwareVersion
!= null && {
444 firmwareVersion
: stationInfo
.firmwareVersion
446 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
447 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
448 ...(stationInfo
.meterSerialNumber
!= null && {
449 meterSerialNumber
: stationInfo
.meterSerialNumber
451 ...(stationInfo
.meterType
!= null && {
452 meterType
: stationInfo
.meterType
454 } satisfies OCPP16BootNotificationRequest
455 case OCPPVersion
.VERSION_20
:
456 case OCPPVersion
.VERSION_201
:
460 model
: stationInfo
.chargePointModel
,
461 vendorName
: stationInfo
.chargePointVendor
,
462 ...(stationInfo
.firmwareVersion
!= null && {
463 firmwareVersion
: stationInfo
.firmwareVersion
465 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
466 serialNumber
: stationInfo
.chargeBoxSerialNumber
468 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
470 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
471 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
475 } satisfies OCPP20BootNotificationRequest
479 export const warnTemplateKeysDeprecation
= (
480 stationTemplate
: ChargingStationTemplate
,
484 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
485 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
486 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
487 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
488 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
490 for (const templateKey
of templateKeys
) {
491 warnDeprecatedTemplateKey(
493 templateKey
.deprecatedKey
,
496 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
498 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
502 export const stationTemplateToStationInfo
= (
503 stationTemplate
: ChargingStationTemplate
504 ): ChargingStationInfo
=> {
505 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
506 delete stationTemplate
.power
507 delete stationTemplate
.powerUnit
508 delete stationTemplate
.Connectors
509 delete stationTemplate
.Evses
510 delete stationTemplate
.Configuration
511 delete stationTemplate
.AutomaticTransactionGenerator
512 delete stationTemplate
.numberOfConnectors
513 delete stationTemplate
.chargeBoxSerialNumberPrefix
514 delete stationTemplate
.chargePointSerialNumberPrefix
515 delete stationTemplate
.meterSerialNumberPrefix
516 return stationTemplate
as ChargingStationInfo
519 export const createSerialNumber
= (
520 stationTemplate
: ChargingStationTemplate
,
521 stationInfo
: ChargingStationInfo
,
523 randomSerialNumberUpperCase
?: boolean
524 randomSerialNumber
?: boolean
527 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
528 const serialNumberSuffix
=
529 params
.randomSerialNumber
=== true
530 ? getRandomSerialNumberSuffix({
531 upperCase
: params
.randomSerialNumberUpperCase
534 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
535 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
536 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
537 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
538 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
539 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
542 export const propagateSerialNumber
= (
543 stationTemplate
: ChargingStationTemplate
| undefined,
544 stationInfoSrc
: ChargingStationInfo
| undefined,
545 stationInfoDst
: ChargingStationInfo
547 if (stationInfoSrc
== null || stationTemplate
== null) {
549 'Missing charging station template or existing configuration to propagate serial number'
552 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
553 stationInfoSrc
.chargePointSerialNumber
!= null
554 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
555 : stationInfoDst
.chargePointSerialNumber
!= null &&
556 delete stationInfoDst
.chargePointSerialNumber
557 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
558 stationInfoSrc
.chargeBoxSerialNumber
!= null
559 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
560 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
561 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
562 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
563 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
566 export const hasFeatureProfile
= (
567 chargingStation
: ChargingStation
,
568 featureProfile
: SupportedFeatureProfiles
569 ): boolean | undefined => {
570 return getConfigurationKey(
572 StandardParametersKey
.SupportedFeatureProfiles
573 )?.value
?.includes(featureProfile
)
576 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
578 switch (stationInfo
.amperageLimitationUnit
) {
579 case AmpereUnits
.DECI_AMPERE
:
582 case AmpereUnits
.CENTI_AMPERE
:
585 case AmpereUnits
.MILLI_AMPERE
:
593 * Gets the connector cloned charging profiles applying a power limitation
594 * and sorted by connector id descending then stack level descending
596 * @param chargingStation -
597 * @param connectorId -
598 * @returns connector charging profiles array
600 export const getConnectorChargingProfiles
= (
601 chargingStation
: ChargingStation
,
603 ): ChargingProfile
[] => {
604 return clone
<ChargingProfile
[]>(
605 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
606 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
608 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
609 (a
, b
) => b
.stackLevel
- a
.stackLevel
615 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
616 chargingStation
: ChargingStation
,
618 ): number | undefined => {
619 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
620 // Get charging profiles sorted by connector id then stack level
621 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
622 if (isNotEmptyArray(chargingProfiles
)) {
623 const result
= getLimitFromChargingProfiles(
627 chargingStation
.logPrefix()
629 if (result
!= null) {
631 chargingProfile
= result
.chargingProfile
632 switch (chargingStation
.stationInfo
?.currentOutType
) {
635 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
637 : ACElectricUtils
.powerTotal(
638 chargingStation
.getNumberOfPhases(),
639 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
640 chargingStation
.stationInfo
.voltageOut
!,
646 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
648 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
649 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
651 const connectorMaximumPower
=
652 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
653 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
654 if (limit
> connectorMaximumPower
) {
656 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
657 chargingProfile.chargingProfileId
658 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
661 limit
= connectorMaximumPower
668 export const getDefaultVoltageOut
= (
669 currentType
: CurrentType
,
673 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
674 let defaultVoltageOut
: number
675 switch (currentType
) {
677 defaultVoltageOut
= Voltage
.VOLTAGE_230
680 defaultVoltageOut
= Voltage
.VOLTAGE_400
683 logger
.error(`${logPrefix} ${errorMsg}`)
684 throw new BaseError(errorMsg
)
686 return defaultVoltageOut
689 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
690 return stationInfo
.idTagsFile
!= null
691 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
695 export const waitChargingStationEvents
= async (
696 emitter
: EventEmitter
,
697 event
: ChargingStationWorkerMessageEvents
,
699 ): Promise
<number> => {
700 return await new Promise
<number>(resolve
=> {
702 if (eventsToWait
=== 0) {
706 emitter
.on(event
, () => {
708 if (events
=== eventsToWait
) {
715 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
716 let configuredMaxNumberOfConnectors
= 0
717 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
718 const numberOfConnectors
= stationTemplate
.numberOfConnectors
719 configuredMaxNumberOfConnectors
=
720 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
721 } else if (stationTemplate
.numberOfConnectors
!= null) {
722 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
723 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
724 configuredMaxNumberOfConnectors
=
725 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
726 stationTemplate
.Connectors
[0] != null
727 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
728 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
729 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
730 for (const evse
in stationTemplate
.Evses
) {
734 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
735 stationTemplate
.Evses
[evse
].Connectors
739 return configuredMaxNumberOfConnectors
742 const checkConfiguredMaxConnectors
= (
743 configuredMaxConnectors
: number,
747 if (configuredMaxConnectors
<= 0) {
749 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
754 const checkTemplateMaxConnectors
= (
755 templateMaxConnectors
: number,
759 if (templateMaxConnectors
=== 0) {
761 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
763 } else if (templateMaxConnectors
< 0) {
765 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
770 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
771 connectorStatus
.availability
= AvailabilityType
.Operative
772 connectorStatus
.idTagLocalAuthorized
= false
773 connectorStatus
.idTagAuthorized
= false
774 connectorStatus
.transactionRemoteStarted
= false
775 connectorStatus
.transactionStarted
= false
776 connectorStatus
.energyActiveImportRegisterValue
= 0
777 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
778 if (connectorStatus
.chargingProfiles
== null) {
779 connectorStatus
.chargingProfiles
= []
783 const warnDeprecatedTemplateKey
= (
784 template
: ChargingStationTemplate
,
787 templateFile
: string,
790 if (template
[key
as keyof ChargingStationTemplate
] != null) {
791 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
792 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
794 logger
.warn(`${logPrefix} ${logMsg}`)
795 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
799 const convertDeprecatedTemplateKey
= (
800 template
: ChargingStationTemplate
,
801 deprecatedKey
: string,
804 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
806 (template
as unknown
as Record
<string, unknown
>)[key
] =
807 template
[deprecatedKey
as keyof ChargingStationTemplate
]
809 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
810 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
814 interface ChargingProfilesLimit
{
816 chargingProfile
: ChargingProfile
820 * Charging profiles shall already be sorted by connector id descending then stack level descending
822 * @param chargingStation -
823 * @param connectorId -
824 * @param chargingProfiles -
826 * @returns ChargingProfilesLimit
828 const getLimitFromChargingProfiles
= (
829 chargingStation
: ChargingStation
,
831 chargingProfiles
: ChargingProfile
[],
833 ): ChargingProfilesLimit
| undefined => {
834 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
835 const currentDate
= new Date()
836 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
837 for (const chargingProfile
of chargingProfiles
) {
838 const chargingSchedule
= chargingProfile
.chargingSchedule
839 if (chargingSchedule
.startSchedule
== null) {
841 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
843 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
844 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
846 if (!isDate(chargingSchedule
.startSchedule
)) {
848 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
850 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
851 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
853 if (chargingSchedule
.duration
== null) {
855 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
857 // OCPP specifies that if duration is not defined, it should be infinite
858 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
860 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
863 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
866 // Check if the charging profile is active
868 isWithinInterval(currentDate
, {
869 start
: chargingSchedule
.startSchedule
,
870 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
873 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
874 const chargingSchedulePeriodCompareFn
= (
875 a
: ChargingSchedulePeriod
,
876 b
: ChargingSchedulePeriod
877 ): number => a
.startPeriod
- b
.startPeriod
879 !isArraySorted
<ChargingSchedulePeriod
>(
880 chargingSchedule
.chargingSchedulePeriod
,
881 chargingSchedulePeriodCompareFn
885 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
887 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
889 // Check if the first schedule period startPeriod property is equal to 0
890 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
892 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
896 // Handle only one schedule period
897 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
898 const result
: ChargingProfilesLimit
= {
899 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
902 logger
.debug(debugLogMsg
, result
)
905 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
906 // Search for the right schedule period
909 chargingSchedulePeriod
910 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
911 // Find the right schedule period
914 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
918 // Found the schedule period: previous is the correct one
919 const result
: ChargingProfilesLimit
= {
920 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
921 limit
: previousChargingSchedulePeriod
!.limit
,
924 logger
.debug(debugLogMsg
, result
)
927 // Keep a reference to previous one
928 previousChargingSchedulePeriod
= chargingSchedulePeriod
929 // Handle the last schedule period within the charging profile duration
931 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
932 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
935 chargingSchedule
.startSchedule
,
936 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
938 chargingSchedule
.startSchedule
939 ) > chargingSchedule
.duration
)
941 const result
: ChargingProfilesLimit
= {
942 limit
: previousChargingSchedulePeriod
.limit
,
945 logger
.debug(debugLogMsg
, result
)
954 export const prepareChargingProfileKind
= (
955 connectorStatus
: ConnectorStatus
| undefined,
956 chargingProfile
: ChargingProfile
,
957 currentDate
: string | number | Date,
960 switch (chargingProfile
.chargingProfileKind
) {
961 case ChargingProfileKindType
.RECURRING
:
962 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
965 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
967 case ChargingProfileKindType
.RELATIVE
:
968 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
970 `${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`
972 delete chargingProfile
.chargingSchedule
.startSchedule
974 if (connectorStatus
?.transactionStarted
=== true) {
975 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
977 // FIXME: Handle relative charging profile duration
983 export const canProceedChargingProfile
= (
984 chargingProfile
: ChargingProfile
,
985 currentDate
: string | number | Date,
989 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
990 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
993 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
994 chargingProfile.chargingProfileId
995 } is not valid for the current date ${
996 isDate(currentDate) ? currentDate.toISOString() : currentDate
1002 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1003 chargingProfile
.chargingSchedule
.duration
== null
1006 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1010 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1012 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1016 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1018 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1025 const canProceedRecurringChargingProfile
= (
1026 chargingProfile
: ChargingProfile
,
1030 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1031 chargingProfile
.recurrencyKind
== null
1034 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1039 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1040 chargingProfile
.chargingSchedule
.startSchedule
== null
1043 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1051 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1053 * @param chargingProfile -
1054 * @param currentDate -
1055 * @param logPrefix -
1057 const prepareRecurringChargingProfile
= (
1058 chargingProfile
: ChargingProfile
,
1059 currentDate
: string | number | Date,
1062 const chargingSchedule
= chargingProfile
.chargingSchedule
1063 let recurringIntervalTranslated
= false
1064 let recurringInterval
: Interval
| undefined
1065 switch (chargingProfile
.recurrencyKind
) {
1066 case RecurrencyKindType
.DAILY
:
1067 recurringInterval
= {
1068 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1069 start
: chargingSchedule
.startSchedule
!,
1070 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1071 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1073 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1075 !isWithinInterval(currentDate
, recurringInterval
) &&
1076 isBefore(recurringInterval
.end
, currentDate
)
1078 chargingSchedule
.startSchedule
= addDays(
1079 recurringInterval
.start
,
1080 differenceInDays(currentDate
, recurringInterval
.start
)
1082 recurringInterval
= {
1083 start
: chargingSchedule
.startSchedule
,
1084 end
: addDays(chargingSchedule
.startSchedule
, 1)
1086 recurringIntervalTranslated
= true
1089 case RecurrencyKindType
.WEEKLY
:
1090 recurringInterval
= {
1091 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1092 start
: chargingSchedule
.startSchedule
!,
1093 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1094 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1096 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1098 !isWithinInterval(currentDate
, recurringInterval
) &&
1099 isBefore(recurringInterval
.end
, currentDate
)
1101 chargingSchedule
.startSchedule
= addWeeks(
1102 recurringInterval
.start
,
1103 differenceInWeeks(currentDate
, recurringInterval
.start
)
1105 recurringInterval
= {
1106 start
: chargingSchedule
.startSchedule
,
1107 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1109 recurringIntervalTranslated
= true
1114 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1117 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1118 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1120 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1121 chargingProfile.recurrencyKind
1122 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1123 recurringInterval?.start as Date
1124 ).toISOString()}, ${toDate(
1125 recurringInterval?.end as Date
1126 ).toISOString()}] has not been properly translated to current date ${
1127 isDate(currentDate) ? currentDate.toISOString() : currentDate
1131 return recurringIntervalTranslated
1134 const checkRecurringChargingProfileDuration
= (
1135 chargingProfile
: ChargingProfile
,
1139 if (chargingProfile
.chargingSchedule
.duration
== null) {
1141 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1142 chargingProfile.chargingProfileKind
1143 } charging profile id ${
1144 chargingProfile.chargingProfileId
1145 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1150 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1152 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1155 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1156 chargingProfile.chargingProfileKind
1157 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1158 chargingProfile.chargingSchedule.duration
1159 } is greater than the recurrency time interval duration ${differenceInSeconds(
1164 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1168 const getRandomSerialNumberSuffix
= (params
?: {
1169 randomBytesLength
?: number
1172 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1173 if (params
?.upperCase
=== true) {
1174 return randomSerialNumberSuffix
.toUpperCase()
1176 return randomSerialNumberSuffix