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
?.persistentConfiguration
!= null) {
342 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
343 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
344 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
345 options
.persistentConfiguration
347 if (options
?.autoStart
!= null) {
348 stationInfo
.autoStart
= options
.autoStart
350 if (options
?.autoRegister
!= null) {
351 stationInfo
.autoRegister
= options
.autoRegister
353 if (options
?.enableStatistics
!= null) {
354 stationInfo
.enableStatistics
= options
.enableStatistics
356 if (options
?.ocppStrictCompliance
!= null) {
357 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
359 if (options
?.stopTransactionsOnStopped
!= null) {
360 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
365 export const initializeConnectorsMapStatus
= (
366 connectors
: Map
<number, ConnectorStatus
>,
369 for (const connectorId
of connectors
.keys()) {
370 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
372 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
373 connectors.get(connectorId)?.transactionId
377 if (connectorId
=== 0) {
378 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
379 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
380 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
381 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
382 connectors
.get(connectorId
)!.chargingProfiles
= []
384 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
385 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
386 initializeConnectorStatus(connectors
.get(connectorId
)!)
391 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
392 if (connectorStatus
== null) {
395 connectorStatus
.chargingProfiles
=
396 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
397 ? connectorStatus
.chargingProfiles
.filter(
398 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
401 connectorStatus
.idTagLocalAuthorized
= false
402 connectorStatus
.idTagAuthorized
= false
403 connectorStatus
.transactionRemoteStarted
= false
404 connectorStatus
.transactionStarted
= false
405 delete connectorStatus
.transactionStart
406 delete connectorStatus
.transactionId
407 delete connectorStatus
.localAuthorizeIdTag
408 delete connectorStatus
.authorizeIdTag
409 delete connectorStatus
.transactionIdTag
410 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
411 delete connectorStatus
.transactionBeginMeterValue
414 export const createBootNotificationRequest
= (
415 stationInfo
: ChargingStationInfo
,
416 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
417 ): BootNotificationRequest
| undefined => {
418 const ocppVersion
= stationInfo
.ocppVersion
419 switch (ocppVersion
) {
420 case OCPPVersion
.VERSION_16
:
422 chargePointModel
: stationInfo
.chargePointModel
,
423 chargePointVendor
: stationInfo
.chargePointVendor
,
424 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
425 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
427 ...(stationInfo
.chargePointSerialNumber
!= null && {
428 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
430 ...(stationInfo
.firmwareVersion
!= null && {
431 firmwareVersion
: stationInfo
.firmwareVersion
433 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
434 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
435 ...(stationInfo
.meterSerialNumber
!= null && {
436 meterSerialNumber
: stationInfo
.meterSerialNumber
438 ...(stationInfo
.meterType
!= null && {
439 meterType
: stationInfo
.meterType
441 } satisfies OCPP16BootNotificationRequest
442 case OCPPVersion
.VERSION_20
:
443 case OCPPVersion
.VERSION_201
:
447 model
: stationInfo
.chargePointModel
,
448 vendorName
: stationInfo
.chargePointVendor
,
449 ...(stationInfo
.firmwareVersion
!= null && {
450 firmwareVersion
: stationInfo
.firmwareVersion
452 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
453 serialNumber
: stationInfo
.chargeBoxSerialNumber
455 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
457 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
458 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
462 } satisfies OCPP20BootNotificationRequest
466 export const warnTemplateKeysDeprecation
= (
467 stationTemplate
: ChargingStationTemplate
,
471 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
472 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
473 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
474 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
475 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
477 for (const templateKey
of templateKeys
) {
478 warnDeprecatedTemplateKey(
480 templateKey
.deprecatedKey
,
483 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
485 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
489 export const stationTemplateToStationInfo
= (
490 stationTemplate
: ChargingStationTemplate
491 ): ChargingStationInfo
=> {
492 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
493 delete stationTemplate
.power
494 delete stationTemplate
.powerUnit
495 delete stationTemplate
.Connectors
496 delete stationTemplate
.Evses
497 delete stationTemplate
.Configuration
498 delete stationTemplate
.AutomaticTransactionGenerator
499 delete stationTemplate
.chargeBoxSerialNumberPrefix
500 delete stationTemplate
.chargePointSerialNumberPrefix
501 delete stationTemplate
.meterSerialNumberPrefix
502 return stationTemplate
as ChargingStationInfo
505 export const createSerialNumber
= (
506 stationTemplate
: ChargingStationTemplate
,
507 stationInfo
: ChargingStationInfo
,
509 randomSerialNumberUpperCase
?: boolean
510 randomSerialNumber
?: boolean
513 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
514 const serialNumberSuffix
=
515 params
.randomSerialNumber
=== true
516 ? getRandomSerialNumberSuffix({
517 upperCase
: params
.randomSerialNumberUpperCase
520 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
521 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
522 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
523 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
524 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
525 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
528 export const propagateSerialNumber
= (
529 stationTemplate
: ChargingStationTemplate
| undefined,
530 stationInfoSrc
: ChargingStationInfo
| undefined,
531 stationInfoDst
: ChargingStationInfo
533 if (stationInfoSrc
== null || stationTemplate
== null) {
535 'Missing charging station template or existing configuration to propagate serial number'
538 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
539 stationInfoSrc
.chargePointSerialNumber
!= null
540 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
541 : stationInfoDst
.chargePointSerialNumber
!= null &&
542 delete stationInfoDst
.chargePointSerialNumber
543 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
544 stationInfoSrc
.chargeBoxSerialNumber
!= null
545 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
546 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
547 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
548 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
549 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
552 export const hasFeatureProfile
= (
553 chargingStation
: ChargingStation
,
554 featureProfile
: SupportedFeatureProfiles
555 ): boolean | undefined => {
556 return getConfigurationKey(
558 StandardParametersKey
.SupportedFeatureProfiles
559 )?.value
?.includes(featureProfile
)
562 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
564 switch (stationInfo
.amperageLimitationUnit
) {
565 case AmpereUnits
.DECI_AMPERE
:
568 case AmpereUnits
.CENTI_AMPERE
:
571 case AmpereUnits
.MILLI_AMPERE
:
579 * Gets the connector cloned charging profiles applying a power limitation
580 * and sorted by connector id descending then stack level descending
582 * @param chargingStation -
583 * @param connectorId -
584 * @returns connector charging profiles array
586 export const getConnectorChargingProfiles
= (
587 chargingStation
: ChargingStation
,
589 ): ChargingProfile
[] => {
590 return clone
<ChargingProfile
[]>(
591 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
592 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
594 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
595 (a
, b
) => b
.stackLevel
- a
.stackLevel
601 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
602 chargingStation
: ChargingStation
,
604 ): number | undefined => {
605 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
606 // Get charging profiles sorted by connector id then stack level
607 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
608 if (isNotEmptyArray(chargingProfiles
)) {
609 const result
= getLimitFromChargingProfiles(
613 chargingStation
.logPrefix()
615 if (result
!= null) {
617 chargingProfile
= result
.chargingProfile
618 switch (chargingStation
.stationInfo
?.currentOutType
) {
621 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
623 : ACElectricUtils
.powerTotal(
624 chargingStation
.getNumberOfPhases(),
625 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
626 chargingStation
.stationInfo
.voltageOut
!,
632 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
634 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
635 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
637 const connectorMaximumPower
=
638 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
639 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
640 if (limit
> connectorMaximumPower
) {
642 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
643 chargingProfile.chargingProfileId
644 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
647 limit
= connectorMaximumPower
654 export const getDefaultVoltageOut
= (
655 currentType
: CurrentType
,
659 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
660 let defaultVoltageOut
: number
661 switch (currentType
) {
663 defaultVoltageOut
= Voltage
.VOLTAGE_230
666 defaultVoltageOut
= Voltage
.VOLTAGE_400
669 logger
.error(`${logPrefix} ${errorMsg}`)
670 throw new BaseError(errorMsg
)
672 return defaultVoltageOut
675 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
676 return stationInfo
.idTagsFile
!= null
677 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
681 export const waitChargingStationEvents
= async (
682 emitter
: EventEmitter
,
683 event
: ChargingStationWorkerMessageEvents
,
685 ): Promise
<number> => {
686 return await new Promise
<number>(resolve
=> {
688 if (eventsToWait
=== 0) {
692 emitter
.on(event
, () => {
694 if (events
=== eventsToWait
) {
701 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
702 let configuredMaxNumberOfConnectors
= 0
703 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
704 const numberOfConnectors
= stationTemplate
.numberOfConnectors
705 configuredMaxNumberOfConnectors
=
706 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
707 } else if (stationTemplate
.numberOfConnectors
!= null) {
708 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
709 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
710 configuredMaxNumberOfConnectors
=
711 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
712 stationTemplate
.Connectors
[0] != null
713 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
714 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
715 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
716 for (const evse
in stationTemplate
.Evses
) {
720 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
721 stationTemplate
.Evses
[evse
].Connectors
725 return configuredMaxNumberOfConnectors
728 const checkConfiguredMaxConnectors
= (
729 configuredMaxConnectors
: number,
733 if (configuredMaxConnectors
<= 0) {
735 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
740 const checkTemplateMaxConnectors
= (
741 templateMaxConnectors
: number,
745 if (templateMaxConnectors
=== 0) {
747 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
749 } else if (templateMaxConnectors
< 0) {
751 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
756 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
757 connectorStatus
.availability
= AvailabilityType
.Operative
758 connectorStatus
.idTagLocalAuthorized
= false
759 connectorStatus
.idTagAuthorized
= false
760 connectorStatus
.transactionRemoteStarted
= false
761 connectorStatus
.transactionStarted
= false
762 connectorStatus
.energyActiveImportRegisterValue
= 0
763 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
764 if (connectorStatus
.chargingProfiles
== null) {
765 connectorStatus
.chargingProfiles
= []
769 const warnDeprecatedTemplateKey
= (
770 template
: ChargingStationTemplate
,
773 templateFile
: string,
776 if (template
[key
as keyof ChargingStationTemplate
] != null) {
777 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
778 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
780 logger
.warn(`${logPrefix} ${logMsg}`)
781 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
785 const convertDeprecatedTemplateKey
= (
786 template
: ChargingStationTemplate
,
787 deprecatedKey
: string,
790 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
792 (template
as unknown
as Record
<string, unknown
>)[key
] =
793 template
[deprecatedKey
as keyof ChargingStationTemplate
]
795 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
796 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
800 interface ChargingProfilesLimit
{
802 chargingProfile
: ChargingProfile
806 * Charging profiles shall already be sorted by connector id descending then stack level descending
808 * @param chargingStation -
809 * @param connectorId -
810 * @param chargingProfiles -
812 * @returns ChargingProfilesLimit
814 const getLimitFromChargingProfiles
= (
815 chargingStation
: ChargingStation
,
817 chargingProfiles
: ChargingProfile
[],
819 ): ChargingProfilesLimit
| undefined => {
820 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
821 const currentDate
= new Date()
822 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
823 for (const chargingProfile
of chargingProfiles
) {
824 const chargingSchedule
= chargingProfile
.chargingSchedule
825 if (chargingSchedule
.startSchedule
== null) {
827 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
829 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
830 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
832 if (!isDate(chargingSchedule
.startSchedule
)) {
834 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
836 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
837 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
839 if (chargingSchedule
.duration
== null) {
841 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
843 // OCPP specifies that if duration is not defined, it should be infinite
844 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
846 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
849 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
852 // Check if the charging profile is active
854 isWithinInterval(currentDate
, {
855 start
: chargingSchedule
.startSchedule
,
856 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
859 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
860 const chargingSchedulePeriodCompareFn
= (
861 a
: ChargingSchedulePeriod
,
862 b
: ChargingSchedulePeriod
863 ): number => a
.startPeriod
- b
.startPeriod
865 !isArraySorted
<ChargingSchedulePeriod
>(
866 chargingSchedule
.chargingSchedulePeriod
,
867 chargingSchedulePeriodCompareFn
871 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
873 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
875 // Check if the first schedule period startPeriod property is equal to 0
876 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
878 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
882 // Handle only one schedule period
883 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
884 const result
: ChargingProfilesLimit
= {
885 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
888 logger
.debug(debugLogMsg
, result
)
891 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
892 // Search for the right schedule period
895 chargingSchedulePeriod
896 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
897 // Find the right schedule period
900 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
904 // Found the schedule period: previous is the correct one
905 const result
: ChargingProfilesLimit
= {
906 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
907 limit
: previousChargingSchedulePeriod
!.limit
,
910 logger
.debug(debugLogMsg
, result
)
913 // Keep a reference to previous one
914 previousChargingSchedulePeriod
= chargingSchedulePeriod
915 // Handle the last schedule period within the charging profile duration
917 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
918 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
921 chargingSchedule
.startSchedule
,
922 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
924 chargingSchedule
.startSchedule
925 ) > chargingSchedule
.duration
)
927 const result
: ChargingProfilesLimit
= {
928 limit
: previousChargingSchedulePeriod
.limit
,
931 logger
.debug(debugLogMsg
, result
)
940 export const prepareChargingProfileKind
= (
941 connectorStatus
: ConnectorStatus
| undefined,
942 chargingProfile
: ChargingProfile
,
943 currentDate
: string | number | Date,
946 switch (chargingProfile
.chargingProfileKind
) {
947 case ChargingProfileKindType
.RECURRING
:
948 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
951 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
953 case ChargingProfileKindType
.RELATIVE
:
954 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
956 `${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`
958 delete chargingProfile
.chargingSchedule
.startSchedule
960 if (connectorStatus
?.transactionStarted
=== true) {
961 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
963 // FIXME: Handle relative charging profile duration
969 export const canProceedChargingProfile
= (
970 chargingProfile
: ChargingProfile
,
971 currentDate
: string | number | Date,
975 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
976 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
979 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
980 chargingProfile.chargingProfileId
981 } is not valid for the current date ${
982 isDate(currentDate) ? currentDate.toISOString() : currentDate
988 chargingProfile
.chargingSchedule
.startSchedule
== null ||
989 chargingProfile
.chargingSchedule
.duration
== null
992 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
996 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
998 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1002 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1004 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1011 const canProceedRecurringChargingProfile
= (
1012 chargingProfile
: ChargingProfile
,
1016 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1017 chargingProfile
.recurrencyKind
== null
1020 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1025 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1026 chargingProfile
.chargingSchedule
.startSchedule
== null
1029 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1037 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1039 * @param chargingProfile -
1040 * @param currentDate -
1041 * @param logPrefix -
1043 const prepareRecurringChargingProfile
= (
1044 chargingProfile
: ChargingProfile
,
1045 currentDate
: string | number | Date,
1048 const chargingSchedule
= chargingProfile
.chargingSchedule
1049 let recurringIntervalTranslated
= false
1050 let recurringInterval
: Interval
| undefined
1051 switch (chargingProfile
.recurrencyKind
) {
1052 case RecurrencyKindType
.DAILY
:
1053 recurringInterval
= {
1054 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1055 start
: chargingSchedule
.startSchedule
!,
1056 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1057 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1059 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1061 !isWithinInterval(currentDate
, recurringInterval
) &&
1062 isBefore(recurringInterval
.end
, currentDate
)
1064 chargingSchedule
.startSchedule
= addDays(
1065 recurringInterval
.start
,
1066 differenceInDays(currentDate
, recurringInterval
.start
)
1068 recurringInterval
= {
1069 start
: chargingSchedule
.startSchedule
,
1070 end
: addDays(chargingSchedule
.startSchedule
, 1)
1072 recurringIntervalTranslated
= true
1075 case RecurrencyKindType
.WEEKLY
:
1076 recurringInterval
= {
1077 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1078 start
: chargingSchedule
.startSchedule
!,
1079 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1080 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1082 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1084 !isWithinInterval(currentDate
, recurringInterval
) &&
1085 isBefore(recurringInterval
.end
, currentDate
)
1087 chargingSchedule
.startSchedule
= addWeeks(
1088 recurringInterval
.start
,
1089 differenceInWeeks(currentDate
, recurringInterval
.start
)
1091 recurringInterval
= {
1092 start
: chargingSchedule
.startSchedule
,
1093 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1095 recurringIntervalTranslated
= true
1100 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1103 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1104 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1106 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1107 chargingProfile.recurrencyKind
1108 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1109 recurringInterval?.start as Date
1110 ).toISOString()}, ${toDate(
1111 recurringInterval?.end as Date
1112 ).toISOString()}] has not been properly translated to current date ${
1113 isDate(currentDate) ? currentDate.toISOString() : currentDate
1117 return recurringIntervalTranslated
1120 const checkRecurringChargingProfileDuration
= (
1121 chargingProfile
: ChargingProfile
,
1125 if (chargingProfile
.chargingSchedule
.duration
== null) {
1127 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1128 chargingProfile.chargingProfileKind
1129 } charging profile id ${
1130 chargingProfile.chargingProfileId
1131 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1136 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1138 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1141 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1142 chargingProfile.chargingProfileKind
1143 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1144 chargingProfile.chargingSchedule.duration
1145 } is greater than the recurrency time interval duration ${differenceInSeconds(
1150 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1154 const getRandomSerialNumberSuffix
= (params
?: {
1155 randomBytesLength
?: number
1158 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1159 if (params
?.upperCase
=== true) {
1160 return randomSerialNumberSuffix
.toUpperCase()
1162 return randomSerialNumberSuffix