1 import { createHash
, randomBytes
} from
'node:crypto'
2 import type { EventEmitter
} from
'node:events'
3 import { basename
, dirname
, isAbsolute
, join
, parse
, relative
, resolve
} 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'
24 import { isEmpty
} from
'rambda'
26 import { BaseError
} from
'../exception/index.js'
30 type BootNotificationRequest
,
33 ChargingProfileKindType
,
35 type ChargingSchedulePeriod
,
36 type ChargingStationConfiguration
,
37 type ChargingStationInfo
,
38 type ChargingStationOptions
,
39 type ChargingStationTemplate
,
40 type ChargingStationWorkerMessageEvents
,
41 ConnectorPhaseRotation
,
46 type OCPP16BootNotificationRequest
,
47 type OCPP20BootNotificationRequest
,
51 ReservationTerminationReason
,
52 StandardParametersKey
,
53 type SupportedFeatureProfiles
,
55 } from
'../types/index.js'
69 } from
'../utils/index.js'
70 import type { ChargingStation
} from
'./ChargingStation.js'
71 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
73 const moduleName
= 'Helpers'
75 export const buildTemplateName
= (templateFile
: string): string => {
76 if (isAbsolute(templateFile
)) {
77 templateFile
= relative(
78 resolve(join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'station-templates')),
82 const templateFileParsedPath
= parse(templateFile
)
83 return join(templateFileParsedPath
.dir
, templateFileParsedPath
.name
)
86 export const getChargingStationId
= (
88 stationTemplate
: ChargingStationTemplate
| undefined
90 if (stationTemplate
== null) {
91 return "Unknown 'chargingStationId'"
93 // In case of multiple instances: add instance index to charging station id
94 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
95 const idSuffix
= stationTemplate
.nameSuffix
?? ''
96 const idStr
= `000000000${index.toString()}`
97 return stationTemplate
.fixedName
=== true
98 ? stationTemplate
.baseName
99 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
104 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
105 return isPast(reservation
.expiryDate
)
108 export const removeExpiredReservations
= async (
109 chargingStation
: ChargingStation
110 ): Promise
<void> => {
111 if (chargingStation
.hasEvses
) {
112 for (const evseStatus
of chargingStation
.evses
.values()) {
113 for (const connectorStatus
of evseStatus
.connectors
.values()) {
115 connectorStatus
.reservation
!= null &&
116 hasReservationExpired(connectorStatus
.reservation
)
118 await chargingStation
.removeReservation(
119 connectorStatus
.reservation
,
120 ReservationTerminationReason
.EXPIRED
126 for (const connectorStatus
of chargingStation
.connectors
.values()) {
128 connectorStatus
.reservation
!= null &&
129 hasReservationExpired(connectorStatus
.reservation
)
131 await chargingStation
.removeReservation(
132 connectorStatus
.reservation
,
133 ReservationTerminationReason
.EXPIRED
140 export const getNumberOfReservableConnectors
= (
141 connectors
: Map
<number, ConnectorStatus
>
143 let numberOfReservableConnectors
= 0
144 for (const [connectorId
, connectorStatus
] of connectors
) {
145 if (connectorId
=== 0) {
148 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
149 ++numberOfReservableConnectors
152 return numberOfReservableConnectors
155 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
156 const chargingStationInfo
= {
157 chargePointModel
: stationTemplate
.chargePointModel
,
158 chargePointVendor
: stationTemplate
.chargePointVendor
,
159 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!= null && {
160 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
162 ...(stationTemplate
.chargePointSerialNumberPrefix
!= null && {
163 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
165 ...(stationTemplate
.meterSerialNumberPrefix
!= null && {
166 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
168 ...(stationTemplate
.meterType
!= null && {
169 meterType
: stationTemplate
.meterType
172 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
173 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
177 export const checkChargingStation
= (
178 chargingStation
: ChargingStation
,
181 if (!chargingStation
.started
&& !chargingStation
.starting
) {
182 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
188 export const getPhaseRotationValue
= (
190 numberOfPhases
: number
191 ): string | undefined => {
193 if (connectorId
=== 0 && numberOfPhases
=== 0) {
194 return `${connectorId}.${ConnectorPhaseRotation.RST}`
195 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
196 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
198 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
199 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
200 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
201 return `${connectorId}.${ConnectorPhaseRotation.RST}`
205 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
209 return Object.keys(evses
).length
212 const getMaxNumberOfConnectors
= (
213 connectors
: Record
<string, ConnectorStatus
> | undefined
215 if (connectors
== null) {
218 return Object.keys(connectors
).length
221 export const getBootConnectorStatus
= (
222 chargingStation
: ChargingStation
,
224 connectorStatus
: ConnectorStatus
225 ): ConnectorStatusEnum
=> {
226 let connectorBootStatus
: ConnectorStatusEnum
228 connectorStatus
.status == null &&
229 (!chargingStation
.isChargingStationAvailable() ||
230 !chargingStation
.isConnectorAvailable(connectorId
))
232 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
233 } else if (connectorStatus
.status == null && connectorStatus
.bootStatus
!= null) {
234 // Set boot status in template at startup
235 connectorBootStatus
= connectorStatus
.bootStatus
236 } else if (connectorStatus
.status != null) {
237 // Set previous status at startup
238 connectorBootStatus
= connectorStatus
.status
240 // Set default status
241 connectorBootStatus
= ConnectorStatusEnum
.Available
243 return connectorBootStatus
246 export const checkTemplate
= (
247 stationTemplate
: ChargingStationTemplate
| undefined,
251 if (stationTemplate
== null) {
252 const errorMsg
= `Failed to read charging station template file ${templateFile}`
253 logger
.error(`${logPrefix} ${errorMsg}`)
254 throw new BaseError(errorMsg
)
256 if (isEmpty(stationTemplate
)) {
257 const errorMsg
= `Empty charging station information from template file ${templateFile}`
258 logger
.error(`${logPrefix} ${errorMsg}`)
259 throw new BaseError(errorMsg
)
261 if (stationTemplate
.idTagsFile
== null || isEmpty(stationTemplate
.idTagsFile
)) {
263 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
268 export const checkConfiguration
= (
269 stationConfiguration
: ChargingStationConfiguration
| undefined,
271 configurationFile
: string
273 if (stationConfiguration
== null) {
274 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
275 logger
.error(`${logPrefix} ${errorMsg}`)
276 throw new BaseError(errorMsg
)
278 if (isEmpty(stationConfiguration
)) {
279 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
280 logger
.error(`${logPrefix} ${errorMsg}`)
281 throw new BaseError(errorMsg
)
285 export const checkConnectorsConfiguration
= (
286 stationTemplate
: ChargingStationTemplate
,
290 configuredMaxConnectors
: number
291 templateMaxConnectors
: number
292 templateMaxAvailableConnectors
: number
294 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
295 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
296 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
297 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
298 const templateMaxAvailableConnectors
=
299 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
301 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
302 stationTemplate
.randomConnectors
!== true
305 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
307 stationTemplate
.randomConnectors
= true
310 configuredMaxConnectors
,
311 templateMaxConnectors
,
312 templateMaxAvailableConnectors
316 export const checkStationInfoConnectorStatus
= (
318 connectorStatus
: ConnectorStatus
,
322 if (connectorStatus
.status != null) {
324 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
326 delete connectorStatus
.status
330 export const buildConnectorsMap
= (
331 connectors
: Record
<string, ConnectorStatus
>,
334 ): Map
<number, ConnectorStatus
> => {
335 const connectorsMap
= new Map
<number, ConnectorStatus
>()
336 if (getMaxNumberOfConnectors(connectors
) > 0) {
337 for (const connector
in connectors
) {
338 const connectorStatus
= connectors
[connector
]
339 const connectorId
= convertToInt(connector
)
340 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
341 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
345 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
351 export const setChargingStationOptions
= (
352 stationInfo
: ChargingStationInfo
,
353 options
?: ChargingStationOptions
354 ): ChargingStationInfo
=> {
355 if (options
?.supervisionUrls
!= null) {
356 stationInfo
.supervisionUrls
= options
.supervisionUrls
358 if (options
?.persistentConfiguration
!= null) {
359 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
360 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
361 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
362 options
.persistentConfiguration
364 if (options
?.autoStart
!= null) {
365 stationInfo
.autoStart
= options
.autoStart
367 if (options
?.autoRegister
!= null) {
368 stationInfo
.autoRegister
= options
.autoRegister
370 if (options
?.enableStatistics
!= null) {
371 stationInfo
.enableStatistics
= options
.enableStatistics
373 if (options
?.ocppStrictCompliance
!= null) {
374 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
376 if (options
?.stopTransactionsOnStopped
!= null) {
377 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
382 export const initializeConnectorsMapStatus
= (
383 connectors
: Map
<number, ConnectorStatus
>,
386 for (const connectorId
of connectors
.keys()) {
387 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
389 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
390 connectors.get(connectorId)?.transactionId
394 if (connectorId
=== 0) {
395 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
396 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
397 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
398 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
399 connectors
.get(connectorId
)!.chargingProfiles
= []
401 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
402 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
403 initializeConnectorStatus(connectors
.get(connectorId
)!)
408 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
409 if (connectorStatus
== null) {
412 connectorStatus
.chargingProfiles
=
413 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
414 ? connectorStatus
.chargingProfiles
.filter(
415 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
418 connectorStatus
.idTagLocalAuthorized
= false
419 connectorStatus
.idTagAuthorized
= false
420 connectorStatus
.transactionRemoteStarted
= false
421 connectorStatus
.transactionStarted
= false
422 delete connectorStatus
.transactionStart
423 delete connectorStatus
.transactionId
424 delete connectorStatus
.localAuthorizeIdTag
425 delete connectorStatus
.authorizeIdTag
426 delete connectorStatus
.transactionIdTag
427 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
428 delete connectorStatus
.transactionBeginMeterValue
431 export const createBootNotificationRequest
= (
432 stationInfo
: ChargingStationInfo
,
433 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
434 ): BootNotificationRequest
| undefined => {
435 const ocppVersion
= stationInfo
.ocppVersion
436 switch (ocppVersion
) {
437 case OCPPVersion
.VERSION_16
:
439 chargePointModel
: stationInfo
.chargePointModel
,
440 chargePointVendor
: stationInfo
.chargePointVendor
,
441 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
442 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
444 ...(stationInfo
.chargePointSerialNumber
!= null && {
445 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
447 ...(stationInfo
.firmwareVersion
!= null && {
448 firmwareVersion
: stationInfo
.firmwareVersion
450 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
451 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
452 ...(stationInfo
.meterSerialNumber
!= null && {
453 meterSerialNumber
: stationInfo
.meterSerialNumber
455 ...(stationInfo
.meterType
!= null && {
456 meterType
: stationInfo
.meterType
458 } satisfies OCPP16BootNotificationRequest
459 case OCPPVersion
.VERSION_20
:
460 case OCPPVersion
.VERSION_201
:
464 model
: stationInfo
.chargePointModel
,
465 vendorName
: stationInfo
.chargePointVendor
,
466 ...(stationInfo
.firmwareVersion
!= null && {
467 firmwareVersion
: stationInfo
.firmwareVersion
469 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
470 serialNumber
: stationInfo
.chargeBoxSerialNumber
472 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
474 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
475 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
479 } satisfies OCPP20BootNotificationRequest
483 export const warnTemplateKeysDeprecation
= (
484 stationTemplate
: ChargingStationTemplate
,
488 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
489 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
490 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
491 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
492 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
494 for (const templateKey
of templateKeys
) {
495 warnDeprecatedTemplateKey(
497 templateKey
.deprecatedKey
,
500 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
502 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
506 export const stationTemplateToStationInfo
= (
507 stationTemplate
: ChargingStationTemplate
508 ): ChargingStationInfo
=> {
509 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
510 delete stationTemplate
.power
511 delete stationTemplate
.powerUnit
512 delete stationTemplate
.Connectors
513 delete stationTemplate
.Evses
514 delete stationTemplate
.Configuration
515 delete stationTemplate
.AutomaticTransactionGenerator
516 delete stationTemplate
.numberOfConnectors
517 delete stationTemplate
.chargeBoxSerialNumberPrefix
518 delete stationTemplate
.chargePointSerialNumberPrefix
519 delete stationTemplate
.meterSerialNumberPrefix
520 return stationTemplate
as ChargingStationInfo
523 export const createSerialNumber
= (
524 stationTemplate
: ChargingStationTemplate
,
525 stationInfo
: ChargingStationInfo
,
527 randomSerialNumberUpperCase
?: boolean
528 randomSerialNumber
?: boolean
532 ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true },
535 const serialNumberSuffix
=
536 params
.randomSerialNumber
=== true
537 ? getRandomSerialNumberSuffix({
538 upperCase
: params
.randomSerialNumberUpperCase
541 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
542 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
543 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
544 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
545 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
546 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
549 export const propagateSerialNumber
= (
550 stationTemplate
: ChargingStationTemplate
| undefined,
551 stationInfoSrc
: ChargingStationInfo
| undefined,
552 stationInfoDst
: ChargingStationInfo
554 if (stationInfoSrc
== null || stationTemplate
== null) {
556 'Missing charging station template or existing configuration to propagate serial number'
559 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
560 stationInfoSrc
.chargePointSerialNumber
!= null
561 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
562 : stationInfoDst
.chargePointSerialNumber
!= null &&
563 delete stationInfoDst
.chargePointSerialNumber
564 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
565 stationInfoSrc
.chargeBoxSerialNumber
!= null
566 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
567 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
568 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
569 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
570 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
573 export const hasFeatureProfile
= (
574 chargingStation
: ChargingStation
,
575 featureProfile
: SupportedFeatureProfiles
576 ): boolean | undefined => {
577 return getConfigurationKey(
579 StandardParametersKey
.SupportedFeatureProfiles
580 )?.value
?.includes(featureProfile
)
583 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
585 switch (stationInfo
.amperageLimitationUnit
) {
586 case AmpereUnits
.DECI_AMPERE
:
589 case AmpereUnits
.CENTI_AMPERE
:
592 case AmpereUnits
.MILLI_AMPERE
:
600 * Gets the connector cloned charging profiles applying a power limitation
601 * and sorted by connector id descending then stack level descending
603 * @param chargingStation -
604 * @param connectorId -
605 * @returns connector charging profiles array
607 export const getConnectorChargingProfiles
= (
608 chargingStation
: ChargingStation
,
610 ): ChargingProfile
[] => {
611 return clone
<ChargingProfile
[]>(
612 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
613 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
615 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
616 (a
, b
) => b
.stackLevel
- a
.stackLevel
622 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
623 chargingStation
: ChargingStation
,
625 ): number | undefined => {
626 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
627 // Get charging profiles sorted by connector id then stack level
628 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
629 if (isNotEmptyArray(chargingProfiles
)) {
630 const result
= getLimitFromChargingProfiles(
634 chargingStation
.logPrefix()
636 if (result
!= null) {
638 chargingProfile
= result
.chargingProfile
639 switch (chargingStation
.stationInfo
?.currentOutType
) {
642 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
644 : ACElectricUtils
.powerTotal(
645 chargingStation
.getNumberOfPhases(),
646 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
647 chargingStation
.stationInfo
.voltageOut
!,
653 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
655 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
656 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
658 const connectorMaximumPower
=
659 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
660 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
661 if (limit
> connectorMaximumPower
) {
663 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
664 chargingProfile.chargingProfileId
665 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
668 limit
= connectorMaximumPower
675 export const getDefaultVoltageOut
= (
676 currentType
: CurrentType
,
680 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
681 let defaultVoltageOut
: number
682 switch (currentType
) {
684 defaultVoltageOut
= Voltage
.VOLTAGE_230
687 defaultVoltageOut
= Voltage
.VOLTAGE_400
690 logger
.error(`${logPrefix} ${errorMsg}`)
691 throw new BaseError(errorMsg
)
693 return defaultVoltageOut
696 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
697 return stationInfo
.idTagsFile
!= null
698 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
702 export const waitChargingStationEvents
= async (
703 emitter
: EventEmitter
,
704 event
: ChargingStationWorkerMessageEvents
,
706 ): Promise
<number> => {
707 return await new Promise
<number>(resolve
=> {
709 if (eventsToWait
=== 0) {
713 emitter
.on(event
, () => {
715 if (events
=== eventsToWait
) {
722 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
723 let configuredMaxNumberOfConnectors
= 0
724 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
725 const numberOfConnectors
= stationTemplate
.numberOfConnectors
726 configuredMaxNumberOfConnectors
=
727 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
728 } else if (stationTemplate
.numberOfConnectors
!= null) {
729 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
730 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
731 configuredMaxNumberOfConnectors
=
732 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
733 stationTemplate
.Connectors
[0] != null
734 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
735 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
736 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
737 for (const evse
in stationTemplate
.Evses
) {
741 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
742 stationTemplate
.Evses
[evse
].Connectors
746 return configuredMaxNumberOfConnectors
749 const checkConfiguredMaxConnectors
= (
750 configuredMaxConnectors
: number,
754 if (configuredMaxConnectors
<= 0) {
756 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
761 const checkTemplateMaxConnectors
= (
762 templateMaxConnectors
: number,
766 if (templateMaxConnectors
=== 0) {
768 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
770 } else if (templateMaxConnectors
< 0) {
772 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
777 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
778 connectorStatus
.availability
= AvailabilityType
.Operative
779 connectorStatus
.idTagLocalAuthorized
= false
780 connectorStatus
.idTagAuthorized
= false
781 connectorStatus
.transactionRemoteStarted
= false
782 connectorStatus
.transactionStarted
= false
783 connectorStatus
.energyActiveImportRegisterValue
= 0
784 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
785 if (connectorStatus
.chargingProfiles
== null) {
786 connectorStatus
.chargingProfiles
= []
790 const warnDeprecatedTemplateKey
= (
791 template
: ChargingStationTemplate
,
794 templateFile
: string,
797 if (template
[key
as keyof ChargingStationTemplate
] != null) {
798 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
799 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
801 logger
.warn(`${logPrefix} ${logMsg}`)
802 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
806 const convertDeprecatedTemplateKey
= (
807 template
: ChargingStationTemplate
,
808 deprecatedKey
: string,
811 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
813 (template
as unknown
as Record
<string, unknown
>)[key
] =
814 template
[deprecatedKey
as keyof ChargingStationTemplate
]
816 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
817 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
821 interface ChargingProfilesLimit
{
823 chargingProfile
: ChargingProfile
827 * Charging profiles shall already be sorted by connector id descending then stack level descending
829 * @param chargingStation -
830 * @param connectorId -
831 * @param chargingProfiles -
833 * @returns ChargingProfilesLimit
835 const getLimitFromChargingProfiles
= (
836 chargingStation
: ChargingStation
,
838 chargingProfiles
: ChargingProfile
[],
840 ): ChargingProfilesLimit
| undefined => {
841 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
842 const currentDate
= new Date()
843 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
844 for (const chargingProfile
of chargingProfiles
) {
845 const chargingSchedule
= chargingProfile
.chargingSchedule
846 if (chargingSchedule
.startSchedule
== null) {
848 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
850 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
851 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
853 if (!isDate(chargingSchedule
.startSchedule
)) {
855 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
857 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
858 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
860 if (chargingSchedule
.duration
== null) {
862 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
864 // OCPP specifies that if duration is not defined, it should be infinite
865 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
867 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
870 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
873 // Check if the charging profile is active
875 isWithinInterval(currentDate
, {
876 start
: chargingSchedule
.startSchedule
,
877 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
880 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
881 const chargingSchedulePeriodCompareFn
= (
882 a
: ChargingSchedulePeriod
,
883 b
: ChargingSchedulePeriod
884 ): number => a
.startPeriod
- b
.startPeriod
886 !isArraySorted
<ChargingSchedulePeriod
>(
887 chargingSchedule
.chargingSchedulePeriod
,
888 chargingSchedulePeriodCompareFn
892 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
894 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
896 // Check if the first schedule period startPeriod property is equal to 0
897 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
899 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
903 // Handle only one schedule period
904 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
905 const result
: ChargingProfilesLimit
= {
906 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
909 logger
.debug(debugLogMsg
, result
)
912 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
913 // Search for the right schedule period
916 chargingSchedulePeriod
917 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
918 // Find the right schedule period
921 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
925 // Found the schedule period: previous is the correct one
926 const result
: ChargingProfilesLimit
= {
927 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
928 limit
: previousChargingSchedulePeriod
!.limit
,
931 logger
.debug(debugLogMsg
, result
)
934 // Keep a reference to previous one
935 previousChargingSchedulePeriod
= chargingSchedulePeriod
936 // Handle the last schedule period within the charging profile duration
938 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
939 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
942 chargingSchedule
.startSchedule
,
943 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
945 chargingSchedule
.startSchedule
946 ) > chargingSchedule
.duration
)
948 const result
: ChargingProfilesLimit
= {
949 limit
: previousChargingSchedulePeriod
.limit
,
952 logger
.debug(debugLogMsg
, result
)
961 export const prepareChargingProfileKind
= (
962 connectorStatus
: ConnectorStatus
| undefined,
963 chargingProfile
: ChargingProfile
,
964 currentDate
: string | number | Date,
967 switch (chargingProfile
.chargingProfileKind
) {
968 case ChargingProfileKindType
.RECURRING
:
969 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
972 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
974 case ChargingProfileKindType
.RELATIVE
:
975 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
977 `${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`
979 delete chargingProfile
.chargingSchedule
.startSchedule
981 if (connectorStatus
?.transactionStarted
=== true) {
982 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
984 // FIXME: handle relative charging profile duration
990 export const canProceedChargingProfile
= (
991 chargingProfile
: ChargingProfile
,
992 currentDate
: string | number | Date,
996 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
997 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1000 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1001 chargingProfile.chargingProfileId
1002 } is not valid for the current date ${
1003 isDate(currentDate) ? currentDate.toISOString() : currentDate
1009 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1010 chargingProfile
.chargingSchedule
.duration
== null
1013 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1017 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1019 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1023 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1025 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1032 const canProceedRecurringChargingProfile
= (
1033 chargingProfile
: ChargingProfile
,
1037 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1038 chargingProfile
.recurrencyKind
== null
1041 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1046 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1047 chargingProfile
.chargingSchedule
.startSchedule
== null
1050 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1058 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1060 * @param chargingProfile -
1061 * @param currentDate -
1062 * @param logPrefix -
1064 const prepareRecurringChargingProfile
= (
1065 chargingProfile
: ChargingProfile
,
1066 currentDate
: string | number | Date,
1069 const chargingSchedule
= chargingProfile
.chargingSchedule
1070 let recurringIntervalTranslated
= false
1071 let recurringInterval
: Interval
| undefined
1072 switch (chargingProfile
.recurrencyKind
) {
1073 case RecurrencyKindType
.DAILY
:
1074 recurringInterval
= {
1075 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1076 start
: chargingSchedule
.startSchedule
!,
1077 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1078 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1080 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1082 !isWithinInterval(currentDate
, recurringInterval
) &&
1083 isBefore(recurringInterval
.end
, currentDate
)
1085 chargingSchedule
.startSchedule
= addDays(
1086 recurringInterval
.start
,
1087 differenceInDays(currentDate
, recurringInterval
.start
)
1089 recurringInterval
= {
1090 start
: chargingSchedule
.startSchedule
,
1091 end
: addDays(chargingSchedule
.startSchedule
, 1)
1093 recurringIntervalTranslated
= true
1096 case RecurrencyKindType
.WEEKLY
:
1097 recurringInterval
= {
1098 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1099 start
: chargingSchedule
.startSchedule
!,
1100 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1101 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1103 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1105 !isWithinInterval(currentDate
, recurringInterval
) &&
1106 isBefore(recurringInterval
.end
, currentDate
)
1108 chargingSchedule
.startSchedule
= addWeeks(
1109 recurringInterval
.start
,
1110 differenceInWeeks(currentDate
, recurringInterval
.start
)
1112 recurringInterval
= {
1113 start
: chargingSchedule
.startSchedule
,
1114 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1116 recurringIntervalTranslated
= true
1121 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1124 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1125 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1127 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1128 chargingProfile.recurrencyKind
1129 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1130 recurringInterval?.start as Date
1131 ).toISOString()}, ${toDate(
1132 recurringInterval?.end as Date
1133 ).toISOString()}] has not been properly translated to current date ${
1134 isDate(currentDate) ? currentDate.toISOString() : currentDate
1138 return recurringIntervalTranslated
1141 const checkRecurringChargingProfileDuration
= (
1142 chargingProfile
: ChargingProfile
,
1146 if (chargingProfile
.chargingSchedule
.duration
== null) {
1148 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1149 chargingProfile.chargingProfileKind
1150 } charging profile id ${
1151 chargingProfile.chargingProfileId
1152 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1157 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1159 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1162 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1163 chargingProfile.chargingProfileKind
1164 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1165 chargingProfile.chargingSchedule.duration
1166 } is greater than the recurrency time interval duration ${differenceInSeconds(
1171 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1175 const getRandomSerialNumberSuffix
= (params
?: {
1176 randomBytesLength
?: number
1179 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1180 if (params
?.upperCase
=== true) {
1181 return randomSerialNumberSuffix
.toUpperCase()
1183 return randomSerialNumberSuffix