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
!== undefined && {
150 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
152 ...(stationTemplate
.chargePointSerialNumberPrefix
!== undefined && {
153 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
155 ...(stationTemplate
.meterSerialNumberPrefix
!== undefined && {
156 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
158 ...(stationTemplate
.meterType
!== undefined && {
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
.ocppPersistentConfiguration
= options
.persistentConfiguration
343 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
344 options
.persistentConfiguration
346 if (options
?.autoRegister
!= null) {
347 stationInfo
.autoRegister
= options
.autoRegister
349 if (options
?.enableStatistics
!= null) {
350 stationInfo
.enableStatistics
= options
.enableStatistics
352 if (options
?.ocppStrictCompliance
!= null) {
353 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
355 if (options
?.stopTransactionsOnStopped
!= null) {
356 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
361 export const initializeConnectorsMapStatus
= (
362 connectors
: Map
<number, ConnectorStatus
>,
365 for (const connectorId
of connectors
.keys()) {
366 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
368 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
369 connectors.get(connectorId)?.transactionId
373 if (connectorId
=== 0) {
374 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
375 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
376 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
377 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
378 connectors
.get(connectorId
)!.chargingProfiles
= []
380 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
381 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
382 initializeConnectorStatus(connectors
.get(connectorId
)!)
387 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
388 if (connectorStatus
== null) {
391 connectorStatus
.chargingProfiles
=
392 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
393 ? connectorStatus
.chargingProfiles
.filter(
394 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
397 connectorStatus
.idTagLocalAuthorized
= false
398 connectorStatus
.idTagAuthorized
= false
399 connectorStatus
.transactionRemoteStarted
= false
400 connectorStatus
.transactionStarted
= false
401 delete connectorStatus
.transactionStart
402 delete connectorStatus
.transactionId
403 delete connectorStatus
.localAuthorizeIdTag
404 delete connectorStatus
.authorizeIdTag
405 delete connectorStatus
.transactionIdTag
406 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
407 delete connectorStatus
.transactionBeginMeterValue
410 export const createBootNotificationRequest
= (
411 stationInfo
: ChargingStationInfo
,
412 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
413 ): BootNotificationRequest
| undefined => {
414 const ocppVersion
= stationInfo
.ocppVersion
415 switch (ocppVersion
) {
416 case OCPPVersion
.VERSION_16
:
418 chargePointModel
: stationInfo
.chargePointModel
,
419 chargePointVendor
: stationInfo
.chargePointVendor
,
420 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
421 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
423 ...(stationInfo
.chargePointSerialNumber
!== undefined && {
424 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
426 ...(stationInfo
.firmwareVersion
!== undefined && {
427 firmwareVersion
: stationInfo
.firmwareVersion
429 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
430 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
}),
431 ...(stationInfo
.meterSerialNumber
!== undefined && {
432 meterSerialNumber
: stationInfo
.meterSerialNumber
434 ...(stationInfo
.meterType
!== undefined && {
435 meterType
: stationInfo
.meterType
437 } satisfies OCPP16BootNotificationRequest
438 case OCPPVersion
.VERSION_20
:
439 case OCPPVersion
.VERSION_201
:
443 model
: stationInfo
.chargePointModel
,
444 vendorName
: stationInfo
.chargePointVendor
,
445 ...(stationInfo
.firmwareVersion
!== undefined && {
446 firmwareVersion
: stationInfo
.firmwareVersion
448 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
449 serialNumber
: stationInfo
.chargeBoxSerialNumber
451 ...((stationInfo
.iccid
!== undefined || stationInfo
.imsi
!== undefined) && {
453 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
454 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
})
458 } satisfies OCPP20BootNotificationRequest
462 export const warnTemplateKeysDeprecation
= (
463 stationTemplate
: ChargingStationTemplate
,
467 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
468 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
469 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
470 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
471 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
473 for (const templateKey
of templateKeys
) {
474 warnDeprecatedTemplateKey(
476 templateKey
.deprecatedKey
,
479 templateKey
.key
!== undefined ? `Use '${templateKey.key}' instead` : undefined
481 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
485 export const stationTemplateToStationInfo
= (
486 stationTemplate
: ChargingStationTemplate
487 ): ChargingStationInfo
=> {
488 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
489 delete stationTemplate
.power
490 delete stationTemplate
.powerUnit
491 delete stationTemplate
.Connectors
492 delete stationTemplate
.Evses
493 delete stationTemplate
.Configuration
494 delete stationTemplate
.AutomaticTransactionGenerator
495 delete stationTemplate
.chargeBoxSerialNumberPrefix
496 delete stationTemplate
.chargePointSerialNumberPrefix
497 delete stationTemplate
.meterSerialNumberPrefix
498 return stationTemplate
as ChargingStationInfo
501 export const createSerialNumber
= (
502 stationTemplate
: ChargingStationTemplate
,
503 stationInfo
: ChargingStationInfo
,
505 randomSerialNumberUpperCase
?: boolean
506 randomSerialNumber
?: boolean
509 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
510 const serialNumberSuffix
=
511 params
.randomSerialNumber
=== true
512 ? getRandomSerialNumberSuffix({
513 upperCase
: params
.randomSerialNumberUpperCase
516 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
517 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
518 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
519 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
520 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
521 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
524 export const propagateSerialNumber
= (
525 stationTemplate
: ChargingStationTemplate
| undefined,
526 stationInfoSrc
: ChargingStationInfo
| undefined,
527 stationInfoDst
: ChargingStationInfo
529 if (stationInfoSrc
== null || stationTemplate
== null) {
531 'Missing charging station template or existing configuration to propagate serial number'
534 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
535 stationInfoSrc
.chargePointSerialNumber
!= null
536 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
537 : stationInfoDst
.chargePointSerialNumber
!= null &&
538 delete stationInfoDst
.chargePointSerialNumber
539 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
540 stationInfoSrc
.chargeBoxSerialNumber
!= null
541 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
542 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
543 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
544 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
545 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
548 export const hasFeatureProfile
= (
549 chargingStation
: ChargingStation
,
550 featureProfile
: SupportedFeatureProfiles
551 ): boolean | undefined => {
552 return getConfigurationKey(
554 StandardParametersKey
.SupportedFeatureProfiles
555 )?.value
?.includes(featureProfile
)
558 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
560 switch (stationInfo
.amperageLimitationUnit
) {
561 case AmpereUnits
.DECI_AMPERE
:
564 case AmpereUnits
.CENTI_AMPERE
:
567 case AmpereUnits
.MILLI_AMPERE
:
575 * Gets the connector cloned charging profiles applying a power limitation
576 * and sorted by connector id descending then stack level descending
578 * @param chargingStation -
579 * @param connectorId -
580 * @returns connector charging profiles array
582 export const getConnectorChargingProfiles
= (
583 chargingStation
: ChargingStation
,
585 ): ChargingProfile
[] => {
586 return clone
<ChargingProfile
[]>(
587 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
588 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
590 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
591 (a
, b
) => b
.stackLevel
- a
.stackLevel
597 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
598 chargingStation
: ChargingStation
,
600 ): number | undefined => {
601 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
602 // Get charging profiles sorted by connector id then stack level
603 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
604 if (isNotEmptyArray(chargingProfiles
)) {
605 const result
= getLimitFromChargingProfiles(
609 chargingStation
.logPrefix()
611 if (result
!= null) {
613 chargingProfile
= result
.chargingProfile
614 switch (chargingStation
.stationInfo
?.currentOutType
) {
617 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
619 : ACElectricUtils
.powerTotal(
620 chargingStation
.getNumberOfPhases(),
621 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
622 chargingStation
.stationInfo
.voltageOut
!,
628 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
630 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
631 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
633 const connectorMaximumPower
=
634 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
635 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
636 if (limit
> connectorMaximumPower
) {
638 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
639 chargingProfile.chargingProfileId
640 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
643 limit
= connectorMaximumPower
650 export const getDefaultVoltageOut
= (
651 currentType
: CurrentType
,
655 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
656 let defaultVoltageOut
: number
657 switch (currentType
) {
659 defaultVoltageOut
= Voltage
.VOLTAGE_230
662 defaultVoltageOut
= Voltage
.VOLTAGE_400
665 logger
.error(`${logPrefix} ${errorMsg}`)
666 throw new BaseError(errorMsg
)
668 return defaultVoltageOut
671 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
672 return stationInfo
.idTagsFile
!= null
673 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
677 export const waitChargingStationEvents
= async (
678 emitter
: EventEmitter
,
679 event
: ChargingStationWorkerMessageEvents
,
681 ): Promise
<number> => {
682 return await new Promise
<number>(resolve
=> {
684 if (eventsToWait
=== 0) {
688 emitter
.on(event
, () => {
690 if (events
=== eventsToWait
) {
697 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
698 let configuredMaxNumberOfConnectors
= 0
699 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
700 const numberOfConnectors
= stationTemplate
.numberOfConnectors
701 configuredMaxNumberOfConnectors
=
702 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
703 } else if (stationTemplate
.numberOfConnectors
!= null) {
704 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
705 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
706 configuredMaxNumberOfConnectors
=
707 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
708 stationTemplate
.Connectors
[0] != null
709 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
710 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
711 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
712 for (const evse
in stationTemplate
.Evses
) {
716 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
717 stationTemplate
.Evses
[evse
].Connectors
721 return configuredMaxNumberOfConnectors
724 const checkConfiguredMaxConnectors
= (
725 configuredMaxConnectors
: number,
729 if (configuredMaxConnectors
<= 0) {
731 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
736 const checkTemplateMaxConnectors
= (
737 templateMaxConnectors
: number,
741 if (templateMaxConnectors
=== 0) {
743 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
745 } else if (templateMaxConnectors
< 0) {
747 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
752 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
753 connectorStatus
.availability
= AvailabilityType
.Operative
754 connectorStatus
.idTagLocalAuthorized
= false
755 connectorStatus
.idTagAuthorized
= false
756 connectorStatus
.transactionRemoteStarted
= false
757 connectorStatus
.transactionStarted
= false
758 connectorStatus
.energyActiveImportRegisterValue
= 0
759 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
760 if (connectorStatus
.chargingProfiles
== null) {
761 connectorStatus
.chargingProfiles
= []
765 const warnDeprecatedTemplateKey
= (
766 template
: ChargingStationTemplate
,
769 templateFile
: string,
772 if (template
[key
as keyof ChargingStationTemplate
] !== undefined) {
773 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
774 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
776 logger
.warn(`${logPrefix} ${logMsg}`)
777 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
781 const convertDeprecatedTemplateKey
= (
782 template
: ChargingStationTemplate
,
783 deprecatedKey
: string,
786 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] !== undefined) {
787 if (key
!== undefined) {
788 (template
as unknown
as Record
<string, unknown
>)[key
] =
789 template
[deprecatedKey
as keyof ChargingStationTemplate
]
791 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
792 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
796 interface ChargingProfilesLimit
{
798 chargingProfile
: ChargingProfile
802 * Charging profiles shall already be sorted by connector id descending then stack level descending
804 * @param chargingStation -
805 * @param connectorId -
806 * @param chargingProfiles -
808 * @returns ChargingProfilesLimit
810 const getLimitFromChargingProfiles
= (
811 chargingStation
: ChargingStation
,
813 chargingProfiles
: ChargingProfile
[],
815 ): ChargingProfilesLimit
| undefined => {
816 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
817 const currentDate
= new Date()
818 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
819 for (const chargingProfile
of chargingProfiles
) {
820 const chargingSchedule
= chargingProfile
.chargingSchedule
821 if (chargingSchedule
.startSchedule
== null) {
823 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
825 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
826 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
828 if (!isDate(chargingSchedule
.startSchedule
)) {
830 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
832 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
833 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
835 if (chargingSchedule
.duration
== null) {
837 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
839 // OCPP specifies that if duration is not defined, it should be infinite
840 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
842 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
845 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
848 // Check if the charging profile is active
850 isWithinInterval(currentDate
, {
851 start
: chargingSchedule
.startSchedule
,
852 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
855 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
856 const chargingSchedulePeriodCompareFn
= (
857 a
: ChargingSchedulePeriod
,
858 b
: ChargingSchedulePeriod
859 ): number => a
.startPeriod
- b
.startPeriod
861 !isArraySorted
<ChargingSchedulePeriod
>(
862 chargingSchedule
.chargingSchedulePeriod
,
863 chargingSchedulePeriodCompareFn
867 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
869 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
871 // Check if the first schedule period startPeriod property is equal to 0
872 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
874 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
878 // Handle only one schedule period
879 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
880 const result
: ChargingProfilesLimit
= {
881 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
884 logger
.debug(debugLogMsg
, result
)
887 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
888 // Search for the right schedule period
891 chargingSchedulePeriod
892 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
893 // Find the right schedule period
896 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
900 // Found the schedule period: previous is the correct one
901 const result
: ChargingProfilesLimit
= {
902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
903 limit
: previousChargingSchedulePeriod
!.limit
,
906 logger
.debug(debugLogMsg
, result
)
909 // Keep a reference to previous one
910 previousChargingSchedulePeriod
= chargingSchedulePeriod
911 // Handle the last schedule period within the charging profile duration
913 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
914 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
917 chargingSchedule
.startSchedule
,
918 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
920 chargingSchedule
.startSchedule
921 ) > chargingSchedule
.duration
)
923 const result
: ChargingProfilesLimit
= {
924 limit
: previousChargingSchedulePeriod
.limit
,
927 logger
.debug(debugLogMsg
, result
)
936 export const prepareChargingProfileKind
= (
937 connectorStatus
: ConnectorStatus
| undefined,
938 chargingProfile
: ChargingProfile
,
939 currentDate
: string | number | Date,
942 switch (chargingProfile
.chargingProfileKind
) {
943 case ChargingProfileKindType
.RECURRING
:
944 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
947 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
949 case ChargingProfileKindType
.RELATIVE
:
950 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
952 `${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`
954 delete chargingProfile
.chargingSchedule
.startSchedule
956 if (connectorStatus
?.transactionStarted
=== true) {
957 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
959 // FIXME: Handle relative charging profile duration
965 export const canProceedChargingProfile
= (
966 chargingProfile
: ChargingProfile
,
967 currentDate
: string | number | Date,
971 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
972 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
975 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
976 chargingProfile.chargingProfileId
977 } is not valid for the current date ${
978 isDate(currentDate) ? currentDate.toISOString() : currentDate
984 chargingProfile
.chargingSchedule
.startSchedule
== null ||
985 chargingProfile
.chargingSchedule
.duration
== null
988 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
992 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
994 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
998 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1000 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1007 const canProceedRecurringChargingProfile
= (
1008 chargingProfile
: ChargingProfile
,
1012 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1013 chargingProfile
.recurrencyKind
== null
1016 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1021 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1022 chargingProfile
.chargingSchedule
.startSchedule
== null
1025 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1033 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1035 * @param chargingProfile -
1036 * @param currentDate -
1037 * @param logPrefix -
1039 const prepareRecurringChargingProfile
= (
1040 chargingProfile
: ChargingProfile
,
1041 currentDate
: string | number | Date,
1044 const chargingSchedule
= chargingProfile
.chargingSchedule
1045 let recurringIntervalTranslated
= false
1046 let recurringInterval
: Interval
| undefined
1047 switch (chargingProfile
.recurrencyKind
) {
1048 case RecurrencyKindType
.DAILY
:
1049 recurringInterval
= {
1050 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1051 start
: chargingSchedule
.startSchedule
!,
1052 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1053 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1055 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1057 !isWithinInterval(currentDate
, recurringInterval
) &&
1058 isBefore(recurringInterval
.end
, currentDate
)
1060 chargingSchedule
.startSchedule
= addDays(
1061 recurringInterval
.start
,
1062 differenceInDays(currentDate
, recurringInterval
.start
)
1064 recurringInterval
= {
1065 start
: chargingSchedule
.startSchedule
,
1066 end
: addDays(chargingSchedule
.startSchedule
, 1)
1068 recurringIntervalTranslated
= true
1071 case RecurrencyKindType
.WEEKLY
:
1072 recurringInterval
= {
1073 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1074 start
: chargingSchedule
.startSchedule
!,
1075 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1076 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1078 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1080 !isWithinInterval(currentDate
, recurringInterval
) &&
1081 isBefore(recurringInterval
.end
, currentDate
)
1083 chargingSchedule
.startSchedule
= addWeeks(
1084 recurringInterval
.start
,
1085 differenceInWeeks(currentDate
, recurringInterval
.start
)
1087 recurringInterval
= {
1088 start
: chargingSchedule
.startSchedule
,
1089 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1091 recurringIntervalTranslated
= true
1096 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1099 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1100 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1102 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1103 chargingProfile.recurrencyKind
1104 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1105 recurringInterval?.start as Date
1106 ).toISOString()}, ${toDate(
1107 recurringInterval?.end as Date
1108 ).toISOString()}] has not been properly translated to current date ${
1109 isDate(currentDate) ? currentDate.toISOString() : currentDate
1113 return recurringIntervalTranslated
1116 const checkRecurringChargingProfileDuration
= (
1117 chargingProfile
: ChargingProfile
,
1121 if (chargingProfile
.chargingSchedule
.duration
== null) {
1123 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1124 chargingProfile.chargingProfileKind
1125 } charging profile id ${
1126 chargingProfile.chargingProfileId
1127 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1132 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1134 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1137 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1138 chargingProfile.chargingProfileKind
1139 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1140 chargingProfile.chargingSchedule.duration
1141 } is greater than the recurrency time interval duration ${differenceInSeconds(
1146 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1150 const getRandomSerialNumberSuffix
= (params
?: {
1151 randomBytesLength
?: number
1154 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1155 if (params
?.upperCase
=== true) {
1156 return randomSerialNumberSuffix
.toUpperCase()
1158 return randomSerialNumberSuffix