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 chargingStation
: ChargingStation
,
339 stationInfo
: ChargingStationInfo
,
340 options
?: ChargingStationOptions
341 ): ChargingStationInfo
=> {
342 if (options
?.supervisionUrls
!= null) {
343 chargingStation
.setSupervisionUrls(options
.supervisionUrls
, false)
345 if (options
?.persistentConfiguration
!= null) {
346 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
347 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
348 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
349 options
.persistentConfiguration
351 if (options
?.autoStart
!= null) {
352 stationInfo
.autoStart
= options
.autoStart
354 if (options
?.autoRegister
!= null) {
355 stationInfo
.autoRegister
= options
.autoRegister
357 if (options
?.enableStatistics
!= null) {
358 stationInfo
.enableStatistics
= options
.enableStatistics
360 if (options
?.ocppStrictCompliance
!= null) {
361 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
363 if (options
?.stopTransactionsOnStopped
!= null) {
364 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
369 export const initializeConnectorsMapStatus
= (
370 connectors
: Map
<number, ConnectorStatus
>,
373 for (const connectorId
of connectors
.keys()) {
374 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
376 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
377 connectors.get(connectorId)?.transactionId
381 if (connectorId
=== 0) {
382 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
383 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
384 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
385 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
386 connectors
.get(connectorId
)!.chargingProfiles
= []
388 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
389 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
390 initializeConnectorStatus(connectors
.get(connectorId
)!)
395 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
396 if (connectorStatus
== null) {
399 connectorStatus
.chargingProfiles
=
400 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
401 ? connectorStatus
.chargingProfiles
.filter(
402 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
405 connectorStatus
.idTagLocalAuthorized
= false
406 connectorStatus
.idTagAuthorized
= false
407 connectorStatus
.transactionRemoteStarted
= false
408 connectorStatus
.transactionStarted
= false
409 delete connectorStatus
.transactionStart
410 delete connectorStatus
.transactionId
411 delete connectorStatus
.localAuthorizeIdTag
412 delete connectorStatus
.authorizeIdTag
413 delete connectorStatus
.transactionIdTag
414 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
415 delete connectorStatus
.transactionBeginMeterValue
418 export const createBootNotificationRequest
= (
419 stationInfo
: ChargingStationInfo
,
420 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
421 ): BootNotificationRequest
| undefined => {
422 const ocppVersion
= stationInfo
.ocppVersion
423 switch (ocppVersion
) {
424 case OCPPVersion
.VERSION_16
:
426 chargePointModel
: stationInfo
.chargePointModel
,
427 chargePointVendor
: stationInfo
.chargePointVendor
,
428 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
429 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
431 ...(stationInfo
.chargePointSerialNumber
!= null && {
432 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
434 ...(stationInfo
.firmwareVersion
!= null && {
435 firmwareVersion
: stationInfo
.firmwareVersion
437 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
438 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
439 ...(stationInfo
.meterSerialNumber
!= null && {
440 meterSerialNumber
: stationInfo
.meterSerialNumber
442 ...(stationInfo
.meterType
!= null && {
443 meterType
: stationInfo
.meterType
445 } satisfies OCPP16BootNotificationRequest
446 case OCPPVersion
.VERSION_20
:
447 case OCPPVersion
.VERSION_201
:
451 model
: stationInfo
.chargePointModel
,
452 vendorName
: stationInfo
.chargePointVendor
,
453 ...(stationInfo
.firmwareVersion
!= null && {
454 firmwareVersion
: stationInfo
.firmwareVersion
456 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
457 serialNumber
: stationInfo
.chargeBoxSerialNumber
459 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
461 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
462 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
466 } satisfies OCPP20BootNotificationRequest
470 export const warnTemplateKeysDeprecation
= (
471 stationTemplate
: ChargingStationTemplate
,
475 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
476 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
477 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
478 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
479 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
481 for (const templateKey
of templateKeys
) {
482 warnDeprecatedTemplateKey(
484 templateKey
.deprecatedKey
,
487 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
489 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
493 export const stationTemplateToStationInfo
= (
494 stationTemplate
: ChargingStationTemplate
495 ): ChargingStationInfo
=> {
496 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
497 delete stationTemplate
.power
498 delete stationTemplate
.powerUnit
499 delete stationTemplate
.Connectors
500 delete stationTemplate
.Evses
501 delete stationTemplate
.Configuration
502 delete stationTemplate
.AutomaticTransactionGenerator
503 delete stationTemplate
.numberOfConnectors
504 delete stationTemplate
.chargeBoxSerialNumberPrefix
505 delete stationTemplate
.chargePointSerialNumberPrefix
506 delete stationTemplate
.meterSerialNumberPrefix
507 return stationTemplate
as ChargingStationInfo
510 export const createSerialNumber
= (
511 stationTemplate
: ChargingStationTemplate
,
512 stationInfo
: ChargingStationInfo
,
514 randomSerialNumberUpperCase
?: boolean
515 randomSerialNumber
?: boolean
518 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
519 const serialNumberSuffix
=
520 params
.randomSerialNumber
=== true
521 ? getRandomSerialNumberSuffix({
522 upperCase
: params
.randomSerialNumberUpperCase
525 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
526 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
527 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
528 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
529 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
530 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
533 export const propagateSerialNumber
= (
534 stationTemplate
: ChargingStationTemplate
| undefined,
535 stationInfoSrc
: ChargingStationInfo
| undefined,
536 stationInfoDst
: ChargingStationInfo
538 if (stationInfoSrc
== null || stationTemplate
== null) {
540 'Missing charging station template or existing configuration to propagate serial number'
543 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
544 stationInfoSrc
.chargePointSerialNumber
!= null
545 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
546 : stationInfoDst
.chargePointSerialNumber
!= null &&
547 delete stationInfoDst
.chargePointSerialNumber
548 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
549 stationInfoSrc
.chargeBoxSerialNumber
!= null
550 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
551 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
552 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
553 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
554 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
557 export const hasFeatureProfile
= (
558 chargingStation
: ChargingStation
,
559 featureProfile
: SupportedFeatureProfiles
560 ): boolean | undefined => {
561 return getConfigurationKey(
563 StandardParametersKey
.SupportedFeatureProfiles
564 )?.value
?.includes(featureProfile
)
567 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
569 switch (stationInfo
.amperageLimitationUnit
) {
570 case AmpereUnits
.DECI_AMPERE
:
573 case AmpereUnits
.CENTI_AMPERE
:
576 case AmpereUnits
.MILLI_AMPERE
:
584 * Gets the connector cloned charging profiles applying a power limitation
585 * and sorted by connector id descending then stack level descending
587 * @param chargingStation -
588 * @param connectorId -
589 * @returns connector charging profiles array
591 export const getConnectorChargingProfiles
= (
592 chargingStation
: ChargingStation
,
594 ): ChargingProfile
[] => {
595 return clone
<ChargingProfile
[]>(
596 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
597 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
599 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
600 (a
, b
) => b
.stackLevel
- a
.stackLevel
606 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
607 chargingStation
: ChargingStation
,
609 ): number | undefined => {
610 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
611 // Get charging profiles sorted by connector id then stack level
612 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
613 if (isNotEmptyArray(chargingProfiles
)) {
614 const result
= getLimitFromChargingProfiles(
618 chargingStation
.logPrefix()
620 if (result
!= null) {
622 chargingProfile
= result
.chargingProfile
623 switch (chargingStation
.stationInfo
?.currentOutType
) {
626 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
628 : ACElectricUtils
.powerTotal(
629 chargingStation
.getNumberOfPhases(),
630 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
631 chargingStation
.stationInfo
.voltageOut
!,
637 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
639 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
640 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
642 const connectorMaximumPower
=
643 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
644 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
645 if (limit
> connectorMaximumPower
) {
647 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
648 chargingProfile.chargingProfileId
649 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
652 limit
= connectorMaximumPower
659 export const getDefaultVoltageOut
= (
660 currentType
: CurrentType
,
664 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
665 let defaultVoltageOut
: number
666 switch (currentType
) {
668 defaultVoltageOut
= Voltage
.VOLTAGE_230
671 defaultVoltageOut
= Voltage
.VOLTAGE_400
674 logger
.error(`${logPrefix} ${errorMsg}`)
675 throw new BaseError(errorMsg
)
677 return defaultVoltageOut
680 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
681 return stationInfo
.idTagsFile
!= null
682 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
686 export const waitChargingStationEvents
= async (
687 emitter
: EventEmitter
,
688 event
: ChargingStationWorkerMessageEvents
,
690 ): Promise
<number> => {
691 return await new Promise
<number>(resolve
=> {
693 if (eventsToWait
=== 0) {
697 emitter
.on(event
, () => {
699 if (events
=== eventsToWait
) {
706 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
707 let configuredMaxNumberOfConnectors
= 0
708 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
709 const numberOfConnectors
= stationTemplate
.numberOfConnectors
710 configuredMaxNumberOfConnectors
=
711 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
712 } else if (stationTemplate
.numberOfConnectors
!= null) {
713 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
714 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
715 configuredMaxNumberOfConnectors
=
716 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
717 stationTemplate
.Connectors
[0] != null
718 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
719 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
720 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
721 for (const evse
in stationTemplate
.Evses
) {
725 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
726 stationTemplate
.Evses
[evse
].Connectors
730 return configuredMaxNumberOfConnectors
733 const checkConfiguredMaxConnectors
= (
734 configuredMaxConnectors
: number,
738 if (configuredMaxConnectors
<= 0) {
740 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
745 const checkTemplateMaxConnectors
= (
746 templateMaxConnectors
: number,
750 if (templateMaxConnectors
=== 0) {
752 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
754 } else if (templateMaxConnectors
< 0) {
756 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
761 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
762 connectorStatus
.availability
= AvailabilityType
.Operative
763 connectorStatus
.idTagLocalAuthorized
= false
764 connectorStatus
.idTagAuthorized
= false
765 connectorStatus
.transactionRemoteStarted
= false
766 connectorStatus
.transactionStarted
= false
767 connectorStatus
.energyActiveImportRegisterValue
= 0
768 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
769 if (connectorStatus
.chargingProfiles
== null) {
770 connectorStatus
.chargingProfiles
= []
774 const warnDeprecatedTemplateKey
= (
775 template
: ChargingStationTemplate
,
778 templateFile
: string,
781 if (template
[key
as keyof ChargingStationTemplate
] != null) {
782 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
783 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
785 logger
.warn(`${logPrefix} ${logMsg}`)
786 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
790 const convertDeprecatedTemplateKey
= (
791 template
: ChargingStationTemplate
,
792 deprecatedKey
: string,
795 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
797 (template
as unknown
as Record
<string, unknown
>)[key
] =
798 template
[deprecatedKey
as keyof ChargingStationTemplate
]
800 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
801 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
805 interface ChargingProfilesLimit
{
807 chargingProfile
: ChargingProfile
811 * Charging profiles shall already be sorted by connector id descending then stack level descending
813 * @param chargingStation -
814 * @param connectorId -
815 * @param chargingProfiles -
817 * @returns ChargingProfilesLimit
819 const getLimitFromChargingProfiles
= (
820 chargingStation
: ChargingStation
,
822 chargingProfiles
: ChargingProfile
[],
824 ): ChargingProfilesLimit
| undefined => {
825 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
826 const currentDate
= new Date()
827 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
828 for (const chargingProfile
of chargingProfiles
) {
829 const chargingSchedule
= chargingProfile
.chargingSchedule
830 if (chargingSchedule
.startSchedule
== null) {
832 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
834 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
835 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
837 if (!isDate(chargingSchedule
.startSchedule
)) {
839 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
841 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
842 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
844 if (chargingSchedule
.duration
== null) {
846 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
848 // OCPP specifies that if duration is not defined, it should be infinite
849 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
851 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
854 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
857 // Check if the charging profile is active
859 isWithinInterval(currentDate
, {
860 start
: chargingSchedule
.startSchedule
,
861 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
864 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
865 const chargingSchedulePeriodCompareFn
= (
866 a
: ChargingSchedulePeriod
,
867 b
: ChargingSchedulePeriod
868 ): number => a
.startPeriod
- b
.startPeriod
870 !isArraySorted
<ChargingSchedulePeriod
>(
871 chargingSchedule
.chargingSchedulePeriod
,
872 chargingSchedulePeriodCompareFn
876 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
878 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
880 // Check if the first schedule period startPeriod property is equal to 0
881 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
883 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
887 // Handle only one schedule period
888 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
889 const result
: ChargingProfilesLimit
= {
890 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
893 logger
.debug(debugLogMsg
, result
)
896 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
897 // Search for the right schedule period
900 chargingSchedulePeriod
901 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
902 // Find the right schedule period
905 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
909 // Found the schedule period: previous is the correct one
910 const result
: ChargingProfilesLimit
= {
911 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
912 limit
: previousChargingSchedulePeriod
!.limit
,
915 logger
.debug(debugLogMsg
, result
)
918 // Keep a reference to previous one
919 previousChargingSchedulePeriod
= chargingSchedulePeriod
920 // Handle the last schedule period within the charging profile duration
922 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
923 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
926 chargingSchedule
.startSchedule
,
927 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
929 chargingSchedule
.startSchedule
930 ) > chargingSchedule
.duration
)
932 const result
: ChargingProfilesLimit
= {
933 limit
: previousChargingSchedulePeriod
.limit
,
936 logger
.debug(debugLogMsg
, result
)
945 export const prepareChargingProfileKind
= (
946 connectorStatus
: ConnectorStatus
| undefined,
947 chargingProfile
: ChargingProfile
,
948 currentDate
: string | number | Date,
951 switch (chargingProfile
.chargingProfileKind
) {
952 case ChargingProfileKindType
.RECURRING
:
953 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
956 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
958 case ChargingProfileKindType
.RELATIVE
:
959 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
961 `${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`
963 delete chargingProfile
.chargingSchedule
.startSchedule
965 if (connectorStatus
?.transactionStarted
=== true) {
966 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
968 // FIXME: Handle relative charging profile duration
974 export const canProceedChargingProfile
= (
975 chargingProfile
: ChargingProfile
,
976 currentDate
: string | number | Date,
980 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
981 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
984 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
985 chargingProfile.chargingProfileId
986 } is not valid for the current date ${
987 isDate(currentDate) ? currentDate.toISOString() : currentDate
993 chargingProfile
.chargingSchedule
.startSchedule
== null ||
994 chargingProfile
.chargingSchedule
.duration
== null
997 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1001 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1003 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1007 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1009 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1016 const canProceedRecurringChargingProfile
= (
1017 chargingProfile
: ChargingProfile
,
1021 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1022 chargingProfile
.recurrencyKind
== null
1025 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1030 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1031 chargingProfile
.chargingSchedule
.startSchedule
== null
1034 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1042 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1044 * @param chargingProfile -
1045 * @param currentDate -
1046 * @param logPrefix -
1048 const prepareRecurringChargingProfile
= (
1049 chargingProfile
: ChargingProfile
,
1050 currentDate
: string | number | Date,
1053 const chargingSchedule
= chargingProfile
.chargingSchedule
1054 let recurringIntervalTranslated
= false
1055 let recurringInterval
: Interval
| undefined
1056 switch (chargingProfile
.recurrencyKind
) {
1057 case RecurrencyKindType
.DAILY
:
1058 recurringInterval
= {
1059 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1060 start
: chargingSchedule
.startSchedule
!,
1061 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1062 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1064 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1066 !isWithinInterval(currentDate
, recurringInterval
) &&
1067 isBefore(recurringInterval
.end
, currentDate
)
1069 chargingSchedule
.startSchedule
= addDays(
1070 recurringInterval
.start
,
1071 differenceInDays(currentDate
, recurringInterval
.start
)
1073 recurringInterval
= {
1074 start
: chargingSchedule
.startSchedule
,
1075 end
: addDays(chargingSchedule
.startSchedule
, 1)
1077 recurringIntervalTranslated
= true
1080 case RecurrencyKindType
.WEEKLY
:
1081 recurringInterval
= {
1082 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1083 start
: chargingSchedule
.startSchedule
!,
1084 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1085 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1087 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1089 !isWithinInterval(currentDate
, recurringInterval
) &&
1090 isBefore(recurringInterval
.end
, currentDate
)
1092 chargingSchedule
.startSchedule
= addWeeks(
1093 recurringInterval
.start
,
1094 differenceInWeeks(currentDate
, recurringInterval
.start
)
1096 recurringInterval
= {
1097 start
: chargingSchedule
.startSchedule
,
1098 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1100 recurringIntervalTranslated
= true
1105 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1108 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1109 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1111 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1112 chargingProfile.recurrencyKind
1113 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1114 recurringInterval?.start as Date
1115 ).toISOString()}, ${toDate(
1116 recurringInterval?.end as Date
1117 ).toISOString()}] has not been properly translated to current date ${
1118 isDate(currentDate) ? currentDate.toISOString() : currentDate
1122 return recurringIntervalTranslated
1125 const checkRecurringChargingProfileDuration
= (
1126 chargingProfile
: ChargingProfile
,
1130 if (chargingProfile
.chargingSchedule
.duration
== null) {
1132 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1133 chargingProfile.chargingProfileKind
1134 } charging profile id ${
1135 chargingProfile.chargingProfileId
1136 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1141 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1143 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1146 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1147 chargingProfile.chargingProfileKind
1148 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1149 chargingProfile.chargingSchedule.duration
1150 } is greater than the recurrency time interval duration ${differenceInSeconds(
1155 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1159 const getRandomSerialNumberSuffix
= (params
?: {
1160 randomBytesLength
?: number
1163 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1164 if (params
?.upperCase
=== true) {
1165 return randomSerialNumberSuffix
.toUpperCase()
1167 return randomSerialNumberSuffix