1 import { createHash
, randomBytes
} from
'node:crypto'
2 import type { EventEmitter
} from
'node:events'
3 import { basename
, dirname
, join
} 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 type { ChargingStation
} from
'./ChargingStation.js'
26 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
27 import { BaseError
} from
'../exception/index.js'
31 type BootNotificationRequest
,
34 ChargingProfileKindType
,
36 type ChargingSchedulePeriod
,
37 type ChargingStationConfiguration
,
38 type ChargingStationInfo
,
39 type ChargingStationOptions
,
40 type ChargingStationTemplate
,
41 type ChargingStationWorkerMessageEvents
,
42 ConnectorPhaseRotation
,
47 type OCPP16BootNotificationRequest
,
48 type OCPP20BootNotificationRequest
,
52 ReservationTerminationReason
,
53 StandardParametersKey
,
54 type SupportedFeatureProfiles
,
56 } from
'../types/index.js'
72 } from
'../utils/index.js'
74 const moduleName
= 'Helpers'
76 export const getChargingStationId
= (
78 stationTemplate
: ChargingStationTemplate
| undefined
80 if (stationTemplate
== null) {
81 return "Unknown 'chargingStationId'"
83 // In case of multiple instances: add instance index to charging station id
84 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
85 const idSuffix
= stationTemplate
.nameSuffix
?? ''
86 const idStr
= `000000000${index.toString()}`
87 return stationTemplate
.fixedName
=== true
88 ? stationTemplate
.baseName
89 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
94 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
95 return isPast(reservation
.expiryDate
)
98 export const removeExpiredReservations
= async (
99 chargingStation
: ChargingStation
100 ): Promise
<void> => {
101 if (chargingStation
.hasEvses
) {
102 for (const evseStatus
of chargingStation
.evses
.values()) {
103 for (const connectorStatus
of evseStatus
.connectors
.values()) {
105 connectorStatus
.reservation
!= null &&
106 hasReservationExpired(connectorStatus
.reservation
)
108 await chargingStation
.removeReservation(
109 connectorStatus
.reservation
,
110 ReservationTerminationReason
.EXPIRED
116 for (const connectorStatus
of chargingStation
.connectors
.values()) {
118 connectorStatus
.reservation
!= null &&
119 hasReservationExpired(connectorStatus
.reservation
)
121 await chargingStation
.removeReservation(
122 connectorStatus
.reservation
,
123 ReservationTerminationReason
.EXPIRED
130 export const getNumberOfReservableConnectors
= (
131 connectors
: Map
<number, ConnectorStatus
>
133 let numberOfReservableConnectors
= 0
134 for (const [connectorId
, connectorStatus
] of connectors
) {
135 if (connectorId
=== 0) {
138 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
139 ++numberOfReservableConnectors
142 return numberOfReservableConnectors
145 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
146 const chargingStationInfo
= {
147 chargePointModel
: stationTemplate
.chargePointModel
,
148 chargePointVendor
: stationTemplate
.chargePointVendor
,
149 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!= null && {
150 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
152 ...(stationTemplate
.chargePointSerialNumberPrefix
!= null && {
153 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
155 ...(stationTemplate
.meterSerialNumberPrefix
!= null && {
156 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
158 ...(stationTemplate
.meterType
!= null && {
159 meterType
: stationTemplate
.meterType
162 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
163 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
167 export const checkChargingStation
= (
168 chargingStation
: ChargingStation
,
171 if (!chargingStation
.started
&& !chargingStation
.starting
) {
172 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
178 export const getPhaseRotationValue
= (
180 numberOfPhases
: number
181 ): string | undefined => {
183 if (connectorId
=== 0 && numberOfPhases
=== 0) {
184 return `${connectorId}.${ConnectorPhaseRotation.RST}`
185 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
186 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
188 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
189 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
190 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
191 return `${connectorId}.${ConnectorPhaseRotation.RST}`
195 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
199 return Object.keys(evses
).length
202 const getMaxNumberOfConnectors
= (
203 connectors
: Record
<string, ConnectorStatus
> | undefined
205 if (connectors
== null) {
208 return Object.keys(connectors
).length
211 export const getBootConnectorStatus
= (
212 chargingStation
: ChargingStation
,
214 connectorStatus
: ConnectorStatus
215 ): ConnectorStatusEnum
=> {
216 let connectorBootStatus
: ConnectorStatusEnum
218 connectorStatus
.status == null &&
219 (!chargingStation
.isChargingStationAvailable() ||
220 !chargingStation
.isConnectorAvailable(connectorId
))
222 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
223 } else if (connectorStatus
.status == null && connectorStatus
.bootStatus
!= null) {
224 // Set boot status in template at startup
225 connectorBootStatus
= connectorStatus
.bootStatus
226 } else if (connectorStatus
.status != null) {
227 // Set previous status at startup
228 connectorBootStatus
= connectorStatus
.status
230 // Set default status
231 connectorBootStatus
= ConnectorStatusEnum
.Available
233 return connectorBootStatus
236 export const checkTemplate
= (
237 stationTemplate
: ChargingStationTemplate
| undefined,
241 if (stationTemplate
== null) {
242 const errorMsg
= `Failed to read charging station template file ${templateFile}`
243 logger
.error(`${logPrefix} ${errorMsg}`)
244 throw new BaseError(errorMsg
)
246 if (isEmptyObject(stationTemplate
)) {
247 const errorMsg
= `Empty charging station information from template file ${templateFile}`
248 logger
.error(`${logPrefix} ${errorMsg}`)
249 throw new BaseError(errorMsg
)
251 if (stationTemplate
.idTagsFile
== null || isEmptyString(stationTemplate
.idTagsFile
)) {
253 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
258 export const checkConfiguration
= (
259 stationConfiguration
: ChargingStationConfiguration
| undefined,
261 configurationFile
: string
263 if (stationConfiguration
== null) {
264 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
265 logger
.error(`${logPrefix} ${errorMsg}`)
266 throw new BaseError(errorMsg
)
268 if (isEmptyObject(stationConfiguration
)) {
269 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
270 logger
.error(`${logPrefix} ${errorMsg}`)
271 throw new BaseError(errorMsg
)
275 export const checkConnectorsConfiguration
= (
276 stationTemplate
: ChargingStationTemplate
,
280 configuredMaxConnectors
: number
281 templateMaxConnectors
: number
282 templateMaxAvailableConnectors
: number
284 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
285 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
286 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
287 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
288 const templateMaxAvailableConnectors
=
289 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
291 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
292 stationTemplate
.randomConnectors
!== true
295 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
297 stationTemplate
.randomConnectors
= true
299 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
}
302 export const checkStationInfoConnectorStatus
= (
304 connectorStatus
: ConnectorStatus
,
308 if (connectorStatus
.status != null) {
310 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
312 delete connectorStatus
.status
316 export const buildConnectorsMap
= (
317 connectors
: Record
<string, ConnectorStatus
>,
320 ): Map
<number, ConnectorStatus
> => {
321 const connectorsMap
= new Map
<number, ConnectorStatus
>()
322 if (getMaxNumberOfConnectors(connectors
) > 0) {
323 for (const connector
in connectors
) {
324 const connectorStatus
= connectors
[connector
]
325 const connectorId
= convertToInt(connector
)
326 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
327 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
331 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
337 export const setChargingStationOptions
= (
338 stationInfo
: ChargingStationInfo
,
339 options
?: ChargingStationOptions
340 ): ChargingStationInfo
=> {
341 if (options
?.supervisionUrls
!= null) {
342 stationInfo
.supervisionUrls
= options
.supervisionUrls
344 if (options
?.persistentConfiguration
!= null) {
345 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
346 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
347 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
348 options
.persistentConfiguration
350 if (options
?.autoStart
!= null) {
351 stationInfo
.autoStart
= options
.autoStart
353 if (options
?.autoRegister
!= null) {
354 stationInfo
.autoRegister
= options
.autoRegister
356 if (options
?.enableStatistics
!= null) {
357 stationInfo
.enableStatistics
= options
.enableStatistics
359 if (options
?.ocppStrictCompliance
!= null) {
360 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
362 if (options
?.stopTransactionsOnStopped
!= null) {
363 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
368 export const initializeConnectorsMapStatus
= (
369 connectors
: Map
<number, ConnectorStatus
>,
372 for (const connectorId
of connectors
.keys()) {
373 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
375 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
376 connectors.get(connectorId)?.transactionId
380 if (connectorId
=== 0) {
381 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
382 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
383 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
384 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
385 connectors
.get(connectorId
)!.chargingProfiles
= []
387 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
388 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
389 initializeConnectorStatus(connectors
.get(connectorId
)!)
394 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
395 if (connectorStatus
== null) {
398 connectorStatus
.chargingProfiles
=
399 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
400 ? connectorStatus
.chargingProfiles
.filter(
401 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
404 connectorStatus
.idTagLocalAuthorized
= false
405 connectorStatus
.idTagAuthorized
= false
406 connectorStatus
.transactionRemoteStarted
= false
407 connectorStatus
.transactionStarted
= false
408 delete connectorStatus
.transactionStart
409 delete connectorStatus
.transactionId
410 delete connectorStatus
.localAuthorizeIdTag
411 delete connectorStatus
.authorizeIdTag
412 delete connectorStatus
.transactionIdTag
413 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
414 delete connectorStatus
.transactionBeginMeterValue
417 export const createBootNotificationRequest
= (
418 stationInfo
: ChargingStationInfo
,
419 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
420 ): BootNotificationRequest
| undefined => {
421 const ocppVersion
= stationInfo
.ocppVersion
422 switch (ocppVersion
) {
423 case OCPPVersion
.VERSION_16
:
425 chargePointModel
: stationInfo
.chargePointModel
,
426 chargePointVendor
: stationInfo
.chargePointVendor
,
427 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
428 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
430 ...(stationInfo
.chargePointSerialNumber
!= null && {
431 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
433 ...(stationInfo
.firmwareVersion
!= null && {
434 firmwareVersion
: stationInfo
.firmwareVersion
436 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
437 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
438 ...(stationInfo
.meterSerialNumber
!= null && {
439 meterSerialNumber
: stationInfo
.meterSerialNumber
441 ...(stationInfo
.meterType
!= null && {
442 meterType
: stationInfo
.meterType
444 } satisfies OCPP16BootNotificationRequest
445 case OCPPVersion
.VERSION_20
:
446 case OCPPVersion
.VERSION_201
:
450 model
: stationInfo
.chargePointModel
,
451 vendorName
: stationInfo
.chargePointVendor
,
452 ...(stationInfo
.firmwareVersion
!= null && {
453 firmwareVersion
: stationInfo
.firmwareVersion
455 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
456 serialNumber
: stationInfo
.chargeBoxSerialNumber
458 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
460 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
461 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
465 } satisfies OCPP20BootNotificationRequest
469 export const warnTemplateKeysDeprecation
= (
470 stationTemplate
: ChargingStationTemplate
,
474 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
475 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
476 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
477 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
478 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
480 for (const templateKey
of templateKeys
) {
481 warnDeprecatedTemplateKey(
483 templateKey
.deprecatedKey
,
486 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
488 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
492 export const stationTemplateToStationInfo
= (
493 stationTemplate
: ChargingStationTemplate
494 ): ChargingStationInfo
=> {
495 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
496 delete stationTemplate
.power
497 delete stationTemplate
.powerUnit
498 delete stationTemplate
.Connectors
499 delete stationTemplate
.Evses
500 delete stationTemplate
.Configuration
501 delete stationTemplate
.AutomaticTransactionGenerator
502 delete stationTemplate
.numberOfConnectors
503 delete stationTemplate
.chargeBoxSerialNumberPrefix
504 delete stationTemplate
.chargePointSerialNumberPrefix
505 delete stationTemplate
.meterSerialNumberPrefix
506 return stationTemplate
as ChargingStationInfo
509 export const createSerialNumber
= (
510 stationTemplate
: ChargingStationTemplate
,
511 stationInfo
: ChargingStationInfo
,
513 randomSerialNumberUpperCase
?: boolean
514 randomSerialNumber
?: boolean
517 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
518 const serialNumberSuffix
=
519 params
.randomSerialNumber
=== true
520 ? getRandomSerialNumberSuffix({
521 upperCase
: params
.randomSerialNumberUpperCase
524 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
525 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
526 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
527 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
528 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
529 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
532 export const propagateSerialNumber
= (
533 stationTemplate
: ChargingStationTemplate
| undefined,
534 stationInfoSrc
: ChargingStationInfo
| undefined,
535 stationInfoDst
: ChargingStationInfo
537 if (stationInfoSrc
== null || stationTemplate
== null) {
539 'Missing charging station template or existing configuration to propagate serial number'
542 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
543 stationInfoSrc
.chargePointSerialNumber
!= null
544 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
545 : stationInfoDst
.chargePointSerialNumber
!= null &&
546 delete stationInfoDst
.chargePointSerialNumber
547 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
548 stationInfoSrc
.chargeBoxSerialNumber
!= null
549 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
550 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
551 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
552 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
553 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
556 export const hasFeatureProfile
= (
557 chargingStation
: ChargingStation
,
558 featureProfile
: SupportedFeatureProfiles
559 ): boolean | undefined => {
560 return getConfigurationKey(
562 StandardParametersKey
.SupportedFeatureProfiles
563 )?.value
?.includes(featureProfile
)
566 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
568 switch (stationInfo
.amperageLimitationUnit
) {
569 case AmpereUnits
.DECI_AMPERE
:
572 case AmpereUnits
.CENTI_AMPERE
:
575 case AmpereUnits
.MILLI_AMPERE
:
583 * Gets the connector cloned charging profiles applying a power limitation
584 * and sorted by connector id descending then stack level descending
586 * @param chargingStation -
587 * @param connectorId -
588 * @returns connector charging profiles array
590 export const getConnectorChargingProfiles
= (
591 chargingStation
: ChargingStation
,
593 ): ChargingProfile
[] => {
594 return clone
<ChargingProfile
[]>(
595 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
596 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
598 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
599 (a
, b
) => b
.stackLevel
- a
.stackLevel
605 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
606 chargingStation
: ChargingStation
,
608 ): number | undefined => {
609 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
610 // Get charging profiles sorted by connector id then stack level
611 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
612 if (isNotEmptyArray(chargingProfiles
)) {
613 const result
= getLimitFromChargingProfiles(
617 chargingStation
.logPrefix()
619 if (result
!= null) {
621 chargingProfile
= result
.chargingProfile
622 switch (chargingStation
.stationInfo
?.currentOutType
) {
625 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
627 : ACElectricUtils
.powerTotal(
628 chargingStation
.getNumberOfPhases(),
629 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
630 chargingStation
.stationInfo
.voltageOut
!,
636 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
638 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
639 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
641 const connectorMaximumPower
=
642 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
643 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
644 if (limit
> connectorMaximumPower
) {
646 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
647 chargingProfile.chargingProfileId
648 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
651 limit
= connectorMaximumPower
658 export const getDefaultVoltageOut
= (
659 currentType
: CurrentType
,
663 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
664 let defaultVoltageOut
: number
665 switch (currentType
) {
667 defaultVoltageOut
= Voltage
.VOLTAGE_230
670 defaultVoltageOut
= Voltage
.VOLTAGE_400
673 logger
.error(`${logPrefix} ${errorMsg}`)
674 throw new BaseError(errorMsg
)
676 return defaultVoltageOut
679 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
680 return stationInfo
.idTagsFile
!= null
681 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
685 export const waitChargingStationEvents
= async (
686 emitter
: EventEmitter
,
687 event
: ChargingStationWorkerMessageEvents
,
689 ): Promise
<number> => {
690 return await new Promise
<number>(resolve
=> {
692 if (eventsToWait
=== 0) {
696 emitter
.on(event
, () => {
698 if (events
=== eventsToWait
) {
705 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
706 let configuredMaxNumberOfConnectors
= 0
707 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
708 const numberOfConnectors
= stationTemplate
.numberOfConnectors
709 configuredMaxNumberOfConnectors
=
710 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
711 } else if (stationTemplate
.numberOfConnectors
!= null) {
712 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
713 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
714 configuredMaxNumberOfConnectors
=
715 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
716 stationTemplate
.Connectors
[0] != null
717 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
718 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
719 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
720 for (const evse
in stationTemplate
.Evses
) {
724 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
725 stationTemplate
.Evses
[evse
].Connectors
729 return configuredMaxNumberOfConnectors
732 const checkConfiguredMaxConnectors
= (
733 configuredMaxConnectors
: number,
737 if (configuredMaxConnectors
<= 0) {
739 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
744 const checkTemplateMaxConnectors
= (
745 templateMaxConnectors
: number,
749 if (templateMaxConnectors
=== 0) {
751 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
753 } else if (templateMaxConnectors
< 0) {
755 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
760 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
761 connectorStatus
.availability
= AvailabilityType
.Operative
762 connectorStatus
.idTagLocalAuthorized
= false
763 connectorStatus
.idTagAuthorized
= false
764 connectorStatus
.transactionRemoteStarted
= false
765 connectorStatus
.transactionStarted
= false
766 connectorStatus
.energyActiveImportRegisterValue
= 0
767 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
768 if (connectorStatus
.chargingProfiles
== null) {
769 connectorStatus
.chargingProfiles
= []
773 const warnDeprecatedTemplateKey
= (
774 template
: ChargingStationTemplate
,
777 templateFile
: string,
780 if (template
[key
as keyof ChargingStationTemplate
] != null) {
781 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
782 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
784 logger
.warn(`${logPrefix} ${logMsg}`)
785 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
789 const convertDeprecatedTemplateKey
= (
790 template
: ChargingStationTemplate
,
791 deprecatedKey
: string,
794 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
796 (template
as unknown
as Record
<string, unknown
>)[key
] =
797 template
[deprecatedKey
as keyof ChargingStationTemplate
]
799 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
800 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
804 interface ChargingProfilesLimit
{
806 chargingProfile
: ChargingProfile
810 * Charging profiles shall already be sorted by connector id descending then stack level descending
812 * @param chargingStation -
813 * @param connectorId -
814 * @param chargingProfiles -
816 * @returns ChargingProfilesLimit
818 const getLimitFromChargingProfiles
= (
819 chargingStation
: ChargingStation
,
821 chargingProfiles
: ChargingProfile
[],
823 ): ChargingProfilesLimit
| undefined => {
824 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
825 const currentDate
= new Date()
826 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
827 for (const chargingProfile
of chargingProfiles
) {
828 const chargingSchedule
= chargingProfile
.chargingSchedule
829 if (chargingSchedule
.startSchedule
== null) {
831 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
833 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
834 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
836 if (!isDate(chargingSchedule
.startSchedule
)) {
838 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
840 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
841 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
843 if (chargingSchedule
.duration
== null) {
845 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
847 // OCPP specifies that if duration is not defined, it should be infinite
848 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
850 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
853 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
856 // Check if the charging profile is active
858 isWithinInterval(currentDate
, {
859 start
: chargingSchedule
.startSchedule
,
860 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
863 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
864 const chargingSchedulePeriodCompareFn
= (
865 a
: ChargingSchedulePeriod
,
866 b
: ChargingSchedulePeriod
867 ): number => a
.startPeriod
- b
.startPeriod
869 !isArraySorted
<ChargingSchedulePeriod
>(
870 chargingSchedule
.chargingSchedulePeriod
,
871 chargingSchedulePeriodCompareFn
875 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
877 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
879 // Check if the first schedule period startPeriod property is equal to 0
880 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
882 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
886 // Handle only one schedule period
887 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
888 const result
: ChargingProfilesLimit
= {
889 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
892 logger
.debug(debugLogMsg
, result
)
895 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
896 // Search for the right schedule period
899 chargingSchedulePeriod
900 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
901 // Find the right schedule period
904 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
908 // Found the schedule period: previous is the correct one
909 const result
: ChargingProfilesLimit
= {
910 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
911 limit
: previousChargingSchedulePeriod
!.limit
,
914 logger
.debug(debugLogMsg
, result
)
917 // Keep a reference to previous one
918 previousChargingSchedulePeriod
= chargingSchedulePeriod
919 // Handle the last schedule period within the charging profile duration
921 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
922 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
925 chargingSchedule
.startSchedule
,
926 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
928 chargingSchedule
.startSchedule
929 ) > chargingSchedule
.duration
)
931 const result
: ChargingProfilesLimit
= {
932 limit
: previousChargingSchedulePeriod
.limit
,
935 logger
.debug(debugLogMsg
, result
)
944 export const prepareChargingProfileKind
= (
945 connectorStatus
: ConnectorStatus
| undefined,
946 chargingProfile
: ChargingProfile
,
947 currentDate
: string | number | Date,
950 switch (chargingProfile
.chargingProfileKind
) {
951 case ChargingProfileKindType
.RECURRING
:
952 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
955 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
957 case ChargingProfileKindType
.RELATIVE
:
958 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
960 `${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`
962 delete chargingProfile
.chargingSchedule
.startSchedule
964 if (connectorStatus
?.transactionStarted
=== true) {
965 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
967 // FIXME: Handle relative charging profile duration
973 export const canProceedChargingProfile
= (
974 chargingProfile
: ChargingProfile
,
975 currentDate
: string | number | Date,
979 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
980 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
983 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
984 chargingProfile.chargingProfileId
985 } is not valid for the current date ${
986 isDate(currentDate) ? currentDate.toISOString() : currentDate
992 chargingProfile
.chargingSchedule
.startSchedule
== null ||
993 chargingProfile
.chargingSchedule
.duration
== null
996 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1000 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1002 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1006 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1008 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1015 const canProceedRecurringChargingProfile
= (
1016 chargingProfile
: ChargingProfile
,
1020 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1021 chargingProfile
.recurrencyKind
== null
1024 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1029 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1030 chargingProfile
.chargingSchedule
.startSchedule
== null
1033 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1041 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1043 * @param chargingProfile -
1044 * @param currentDate -
1045 * @param logPrefix -
1047 const prepareRecurringChargingProfile
= (
1048 chargingProfile
: ChargingProfile
,
1049 currentDate
: string | number | Date,
1052 const chargingSchedule
= chargingProfile
.chargingSchedule
1053 let recurringIntervalTranslated
= false
1054 let recurringInterval
: Interval
| undefined
1055 switch (chargingProfile
.recurrencyKind
) {
1056 case RecurrencyKindType
.DAILY
:
1057 recurringInterval
= {
1058 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1059 start
: chargingSchedule
.startSchedule
!,
1060 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1061 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1063 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1065 !isWithinInterval(currentDate
, recurringInterval
) &&
1066 isBefore(recurringInterval
.end
, currentDate
)
1068 chargingSchedule
.startSchedule
= addDays(
1069 recurringInterval
.start
,
1070 differenceInDays(currentDate
, recurringInterval
.start
)
1072 recurringInterval
= {
1073 start
: chargingSchedule
.startSchedule
,
1074 end
: addDays(chargingSchedule
.startSchedule
, 1)
1076 recurringIntervalTranslated
= true
1079 case RecurrencyKindType
.WEEKLY
:
1080 recurringInterval
= {
1081 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1082 start
: chargingSchedule
.startSchedule
!,
1083 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1084 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1086 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1088 !isWithinInterval(currentDate
, recurringInterval
) &&
1089 isBefore(recurringInterval
.end
, currentDate
)
1091 chargingSchedule
.startSchedule
= addWeeks(
1092 recurringInterval
.start
,
1093 differenceInWeeks(currentDate
, recurringInterval
.start
)
1095 recurringInterval
= {
1096 start
: chargingSchedule
.startSchedule
,
1097 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1099 recurringIntervalTranslated
= true
1104 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1107 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1108 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1110 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1111 chargingProfile.recurrencyKind
1112 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1113 recurringInterval?.start as Date
1114 ).toISOString()}, ${toDate(
1115 recurringInterval?.end as Date
1116 ).toISOString()}] has not been properly translated to current date ${
1117 isDate(currentDate) ? currentDate.toISOString() : currentDate
1121 return recurringIntervalTranslated
1124 const checkRecurringChargingProfileDuration
= (
1125 chargingProfile
: ChargingProfile
,
1129 if (chargingProfile
.chargingSchedule
.duration
== null) {
1131 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1132 chargingProfile.chargingProfileKind
1133 } charging profile id ${
1134 chargingProfile.chargingProfileId
1135 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1140 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1142 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1145 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1146 chargingProfile.chargingProfileKind
1147 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1148 chargingProfile.chargingSchedule.duration
1149 } is greater than the recurrency time interval duration ${differenceInSeconds(
1154 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1158 const getRandomSerialNumberSuffix
= (params
?: {
1159 randomBytesLength
?: number
1162 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1163 if (params
?.upperCase
=== true) {
1164 return randomSerialNumberSuffix
.toUpperCase()
1166 return randomSerialNumberSuffix