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
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
343 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
344 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
345 options
.persistentConfiguration
347 if (options
?.autoRegister
!= null) {
348 stationInfo
.autoRegister
= options
.autoRegister
350 if (options
?.enableStatistics
!= null) {
351 stationInfo
.enableStatistics
= options
.enableStatistics
353 if (options
?.ocppStrictCompliance
!= null) {
354 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
356 if (options
?.stopTransactionsOnStopped
!= null) {
357 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
362 export const initializeConnectorsMapStatus
= (
363 connectors
: Map
<number, ConnectorStatus
>,
366 for (const connectorId
of connectors
.keys()) {
367 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
369 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
370 connectors.get(connectorId)?.transactionId
374 if (connectorId
=== 0) {
375 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
376 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
377 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
378 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
379 connectors
.get(connectorId
)!.chargingProfiles
= []
381 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
382 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
383 initializeConnectorStatus(connectors
.get(connectorId
)!)
388 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
389 if (connectorStatus
== null) {
392 connectorStatus
.chargingProfiles
=
393 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
394 ? connectorStatus
.chargingProfiles
.filter(
395 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
398 connectorStatus
.idTagLocalAuthorized
= false
399 connectorStatus
.idTagAuthorized
= false
400 connectorStatus
.transactionRemoteStarted
= false
401 connectorStatus
.transactionStarted
= false
402 delete connectorStatus
.transactionStart
403 delete connectorStatus
.transactionId
404 delete connectorStatus
.localAuthorizeIdTag
405 delete connectorStatus
.authorizeIdTag
406 delete connectorStatus
.transactionIdTag
407 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
408 delete connectorStatus
.transactionBeginMeterValue
411 export const createBootNotificationRequest
= (
412 stationInfo
: ChargingStationInfo
,
413 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
414 ): BootNotificationRequest
| undefined => {
415 const ocppVersion
= stationInfo
.ocppVersion
416 switch (ocppVersion
) {
417 case OCPPVersion
.VERSION_16
:
419 chargePointModel
: stationInfo
.chargePointModel
,
420 chargePointVendor
: stationInfo
.chargePointVendor
,
421 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
422 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
424 ...(stationInfo
.chargePointSerialNumber
!== undefined && {
425 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
427 ...(stationInfo
.firmwareVersion
!== undefined && {
428 firmwareVersion
: stationInfo
.firmwareVersion
430 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
431 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
}),
432 ...(stationInfo
.meterSerialNumber
!== undefined && {
433 meterSerialNumber
: stationInfo
.meterSerialNumber
435 ...(stationInfo
.meterType
!== undefined && {
436 meterType
: stationInfo
.meterType
438 } satisfies OCPP16BootNotificationRequest
439 case OCPPVersion
.VERSION_20
:
440 case OCPPVersion
.VERSION_201
:
444 model
: stationInfo
.chargePointModel
,
445 vendorName
: stationInfo
.chargePointVendor
,
446 ...(stationInfo
.firmwareVersion
!== undefined && {
447 firmwareVersion
: stationInfo
.firmwareVersion
449 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
450 serialNumber
: stationInfo
.chargeBoxSerialNumber
452 ...((stationInfo
.iccid
!== undefined || stationInfo
.imsi
!== undefined) && {
454 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
455 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
})
459 } satisfies OCPP20BootNotificationRequest
463 export const warnTemplateKeysDeprecation
= (
464 stationTemplate
: ChargingStationTemplate
,
468 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
469 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
470 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
471 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
472 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
474 for (const templateKey
of templateKeys
) {
475 warnDeprecatedTemplateKey(
477 templateKey
.deprecatedKey
,
480 templateKey
.key
!== undefined ? `Use '${templateKey.key}' instead` : undefined
482 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
486 export const stationTemplateToStationInfo
= (
487 stationTemplate
: ChargingStationTemplate
488 ): ChargingStationInfo
=> {
489 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
490 delete stationTemplate
.power
491 delete stationTemplate
.powerUnit
492 delete stationTemplate
.Connectors
493 delete stationTemplate
.Evses
494 delete stationTemplate
.Configuration
495 delete stationTemplate
.AutomaticTransactionGenerator
496 delete stationTemplate
.chargeBoxSerialNumberPrefix
497 delete stationTemplate
.chargePointSerialNumberPrefix
498 delete stationTemplate
.meterSerialNumberPrefix
499 return stationTemplate
as ChargingStationInfo
502 export const createSerialNumber
= (
503 stationTemplate
: ChargingStationTemplate
,
504 stationInfo
: ChargingStationInfo
,
506 randomSerialNumberUpperCase
?: boolean
507 randomSerialNumber
?: boolean
510 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
511 const serialNumberSuffix
=
512 params
.randomSerialNumber
=== true
513 ? getRandomSerialNumberSuffix({
514 upperCase
: params
.randomSerialNumberUpperCase
517 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
518 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
519 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
520 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
521 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
522 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
525 export const propagateSerialNumber
= (
526 stationTemplate
: ChargingStationTemplate
| undefined,
527 stationInfoSrc
: ChargingStationInfo
| undefined,
528 stationInfoDst
: ChargingStationInfo
530 if (stationInfoSrc
== null || stationTemplate
== null) {
532 'Missing charging station template or existing configuration to propagate serial number'
535 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
536 stationInfoSrc
.chargePointSerialNumber
!= null
537 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
538 : stationInfoDst
.chargePointSerialNumber
!= null &&
539 delete stationInfoDst
.chargePointSerialNumber
540 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
541 stationInfoSrc
.chargeBoxSerialNumber
!= null
542 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
543 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
544 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
545 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
546 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
549 export const hasFeatureProfile
= (
550 chargingStation
: ChargingStation
,
551 featureProfile
: SupportedFeatureProfiles
552 ): boolean | undefined => {
553 return getConfigurationKey(
555 StandardParametersKey
.SupportedFeatureProfiles
556 )?.value
?.includes(featureProfile
)
559 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
561 switch (stationInfo
.amperageLimitationUnit
) {
562 case AmpereUnits
.DECI_AMPERE
:
565 case AmpereUnits
.CENTI_AMPERE
:
568 case AmpereUnits
.MILLI_AMPERE
:
576 * Gets the connector cloned charging profiles applying a power limitation
577 * and sorted by connector id descending then stack level descending
579 * @param chargingStation -
580 * @param connectorId -
581 * @returns connector charging profiles array
583 export const getConnectorChargingProfiles
= (
584 chargingStation
: ChargingStation
,
586 ): ChargingProfile
[] => {
587 return clone
<ChargingProfile
[]>(
588 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
589 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
591 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
592 (a
, b
) => b
.stackLevel
- a
.stackLevel
598 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
599 chargingStation
: ChargingStation
,
601 ): number | undefined => {
602 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
603 // Get charging profiles sorted by connector id then stack level
604 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
605 if (isNotEmptyArray(chargingProfiles
)) {
606 const result
= getLimitFromChargingProfiles(
610 chargingStation
.logPrefix()
612 if (result
!= null) {
614 chargingProfile
= result
.chargingProfile
615 switch (chargingStation
.stationInfo
?.currentOutType
) {
618 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
620 : ACElectricUtils
.powerTotal(
621 chargingStation
.getNumberOfPhases(),
622 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
623 chargingStation
.stationInfo
.voltageOut
!,
629 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
631 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
632 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
634 const connectorMaximumPower
=
635 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
636 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
637 if (limit
> connectorMaximumPower
) {
639 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
640 chargingProfile.chargingProfileId
641 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
644 limit
= connectorMaximumPower
651 export const getDefaultVoltageOut
= (
652 currentType
: CurrentType
,
656 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
657 let defaultVoltageOut
: number
658 switch (currentType
) {
660 defaultVoltageOut
= Voltage
.VOLTAGE_230
663 defaultVoltageOut
= Voltage
.VOLTAGE_400
666 logger
.error(`${logPrefix} ${errorMsg}`)
667 throw new BaseError(errorMsg
)
669 return defaultVoltageOut
672 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
673 return stationInfo
.idTagsFile
!= null
674 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
678 export const waitChargingStationEvents
= async (
679 emitter
: EventEmitter
,
680 event
: ChargingStationWorkerMessageEvents
,
682 ): Promise
<number> => {
683 return await new Promise
<number>(resolve
=> {
685 if (eventsToWait
=== 0) {
689 emitter
.on(event
, () => {
691 if (events
=== eventsToWait
) {
698 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
699 let configuredMaxNumberOfConnectors
= 0
700 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
701 const numberOfConnectors
= stationTemplate
.numberOfConnectors
702 configuredMaxNumberOfConnectors
=
703 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
704 } else if (stationTemplate
.numberOfConnectors
!= null) {
705 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
706 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
707 configuredMaxNumberOfConnectors
=
708 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
709 stationTemplate
.Connectors
[0] != null
710 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
711 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
712 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
713 for (const evse
in stationTemplate
.Evses
) {
717 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
718 stationTemplate
.Evses
[evse
].Connectors
722 return configuredMaxNumberOfConnectors
725 const checkConfiguredMaxConnectors
= (
726 configuredMaxConnectors
: number,
730 if (configuredMaxConnectors
<= 0) {
732 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
737 const checkTemplateMaxConnectors
= (
738 templateMaxConnectors
: number,
742 if (templateMaxConnectors
=== 0) {
744 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
746 } else if (templateMaxConnectors
< 0) {
748 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
753 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
754 connectorStatus
.availability
= AvailabilityType
.Operative
755 connectorStatus
.idTagLocalAuthorized
= false
756 connectorStatus
.idTagAuthorized
= false
757 connectorStatus
.transactionRemoteStarted
= false
758 connectorStatus
.transactionStarted
= false
759 connectorStatus
.energyActiveImportRegisterValue
= 0
760 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
761 if (connectorStatus
.chargingProfiles
== null) {
762 connectorStatus
.chargingProfiles
= []
766 const warnDeprecatedTemplateKey
= (
767 template
: ChargingStationTemplate
,
770 templateFile
: string,
773 if (template
[key
as keyof ChargingStationTemplate
] !== undefined) {
774 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
775 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
777 logger
.warn(`${logPrefix} ${logMsg}`)
778 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
782 const convertDeprecatedTemplateKey
= (
783 template
: ChargingStationTemplate
,
784 deprecatedKey
: string,
787 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] !== undefined) {
788 if (key
!== undefined) {
789 (template
as unknown
as Record
<string, unknown
>)[key
] =
790 template
[deprecatedKey
as keyof ChargingStationTemplate
]
792 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
793 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
797 interface ChargingProfilesLimit
{
799 chargingProfile
: ChargingProfile
803 * Charging profiles shall already be sorted by connector id descending then stack level descending
805 * @param chargingStation -
806 * @param connectorId -
807 * @param chargingProfiles -
809 * @returns ChargingProfilesLimit
811 const getLimitFromChargingProfiles
= (
812 chargingStation
: ChargingStation
,
814 chargingProfiles
: ChargingProfile
[],
816 ): ChargingProfilesLimit
| undefined => {
817 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
818 const currentDate
= new Date()
819 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
820 for (const chargingProfile
of chargingProfiles
) {
821 const chargingSchedule
= chargingProfile
.chargingSchedule
822 if (chargingSchedule
.startSchedule
== null) {
824 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
826 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
827 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
829 if (!isDate(chargingSchedule
.startSchedule
)) {
831 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
833 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
834 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
836 if (chargingSchedule
.duration
== null) {
838 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
840 // OCPP specifies that if duration is not defined, it should be infinite
841 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
843 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
846 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
849 // Check if the charging profile is active
851 isWithinInterval(currentDate
, {
852 start
: chargingSchedule
.startSchedule
,
853 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
856 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
857 const chargingSchedulePeriodCompareFn
= (
858 a
: ChargingSchedulePeriod
,
859 b
: ChargingSchedulePeriod
860 ): number => a
.startPeriod
- b
.startPeriod
862 !isArraySorted
<ChargingSchedulePeriod
>(
863 chargingSchedule
.chargingSchedulePeriod
,
864 chargingSchedulePeriodCompareFn
868 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
870 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
872 // Check if the first schedule period startPeriod property is equal to 0
873 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
875 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
879 // Handle only one schedule period
880 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
881 const result
: ChargingProfilesLimit
= {
882 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
885 logger
.debug(debugLogMsg
, result
)
888 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
889 // Search for the right schedule period
892 chargingSchedulePeriod
893 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
894 // Find the right schedule period
897 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
901 // Found the schedule period: previous is the correct one
902 const result
: ChargingProfilesLimit
= {
903 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
904 limit
: previousChargingSchedulePeriod
!.limit
,
907 logger
.debug(debugLogMsg
, result
)
910 // Keep a reference to previous one
911 previousChargingSchedulePeriod
= chargingSchedulePeriod
912 // Handle the last schedule period within the charging profile duration
914 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
915 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
918 chargingSchedule
.startSchedule
,
919 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
921 chargingSchedule
.startSchedule
922 ) > chargingSchedule
.duration
)
924 const result
: ChargingProfilesLimit
= {
925 limit
: previousChargingSchedulePeriod
.limit
,
928 logger
.debug(debugLogMsg
, result
)
937 export const prepareChargingProfileKind
= (
938 connectorStatus
: ConnectorStatus
| undefined,
939 chargingProfile
: ChargingProfile
,
940 currentDate
: string | number | Date,
943 switch (chargingProfile
.chargingProfileKind
) {
944 case ChargingProfileKindType
.RECURRING
:
945 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
948 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
950 case ChargingProfileKindType
.RELATIVE
:
951 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
953 `${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`
955 delete chargingProfile
.chargingSchedule
.startSchedule
957 if (connectorStatus
?.transactionStarted
=== true) {
958 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
960 // FIXME: Handle relative charging profile duration
966 export const canProceedChargingProfile
= (
967 chargingProfile
: ChargingProfile
,
968 currentDate
: string | number | Date,
972 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
973 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
976 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
977 chargingProfile.chargingProfileId
978 } is not valid for the current date ${
979 isDate(currentDate) ? currentDate.toISOString() : currentDate
985 chargingProfile
.chargingSchedule
.startSchedule
== null ||
986 chargingProfile
.chargingSchedule
.duration
== null
989 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
993 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
995 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
999 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1001 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1008 const canProceedRecurringChargingProfile
= (
1009 chargingProfile
: ChargingProfile
,
1013 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1014 chargingProfile
.recurrencyKind
== null
1017 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1022 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1023 chargingProfile
.chargingSchedule
.startSchedule
== null
1026 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1034 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1036 * @param chargingProfile -
1037 * @param currentDate -
1038 * @param logPrefix -
1040 const prepareRecurringChargingProfile
= (
1041 chargingProfile
: ChargingProfile
,
1042 currentDate
: string | number | Date,
1045 const chargingSchedule
= chargingProfile
.chargingSchedule
1046 let recurringIntervalTranslated
= false
1047 let recurringInterval
: Interval
| undefined
1048 switch (chargingProfile
.recurrencyKind
) {
1049 case RecurrencyKindType
.DAILY
:
1050 recurringInterval
= {
1051 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1052 start
: chargingSchedule
.startSchedule
!,
1053 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1054 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1056 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1058 !isWithinInterval(currentDate
, recurringInterval
) &&
1059 isBefore(recurringInterval
.end
, currentDate
)
1061 chargingSchedule
.startSchedule
= addDays(
1062 recurringInterval
.start
,
1063 differenceInDays(currentDate
, recurringInterval
.start
)
1065 recurringInterval
= {
1066 start
: chargingSchedule
.startSchedule
,
1067 end
: addDays(chargingSchedule
.startSchedule
, 1)
1069 recurringIntervalTranslated
= true
1072 case RecurrencyKindType
.WEEKLY
:
1073 recurringInterval
= {
1074 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1075 start
: chargingSchedule
.startSchedule
!,
1076 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1077 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1079 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1081 !isWithinInterval(currentDate
, recurringInterval
) &&
1082 isBefore(recurringInterval
.end
, currentDate
)
1084 chargingSchedule
.startSchedule
= addWeeks(
1085 recurringInterval
.start
,
1086 differenceInWeeks(currentDate
, recurringInterval
.start
)
1088 recurringInterval
= {
1089 start
: chargingSchedule
.startSchedule
,
1090 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1092 recurringIntervalTranslated
= true
1097 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1100 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1101 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1103 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1104 chargingProfile.recurrencyKind
1105 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1106 recurringInterval?.start as Date
1107 ).toISOString()}, ${toDate(
1108 recurringInterval?.end as Date
1109 ).toISOString()}] has not been properly translated to current date ${
1110 isDate(currentDate) ? currentDate.toISOString() : currentDate
1114 return recurringIntervalTranslated
1117 const checkRecurringChargingProfileDuration
= (
1118 chargingProfile
: ChargingProfile
,
1122 if (chargingProfile
.chargingSchedule
.duration
== null) {
1124 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1125 chargingProfile.chargingProfileKind
1126 } charging profile id ${
1127 chargingProfile.chargingProfileId
1128 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1133 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1135 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1138 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1139 chargingProfile.chargingProfileKind
1140 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1141 chargingProfile.chargingSchedule.duration
1142 } is greater than the recurrency time interval duration ${differenceInSeconds(
1147 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1151 const getRandomSerialNumberSuffix
= (params
?: {
1152 randomBytesLength
?: number
1155 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1156 if (params
?.upperCase
=== true) {
1157 return randomSerialNumberSuffix
.toUpperCase()
1159 return randomSerialNumberSuffix