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
.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