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 resetAuthorizeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
409 connectorStatus
.idTagLocalAuthorized
= false
410 connectorStatus
.idTagAuthorized
= false
411 delete connectorStatus
.localAuthorizeIdTag
412 delete connectorStatus
.authorizeIdTag
415 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
416 if (connectorStatus
== null) {
419 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
420 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
.filter(
422 (chargingProfile
.transactionId
!= null &&
423 connectorStatus
.transactionId
!= null &&
424 chargingProfile
.transactionId
!== connectorStatus
.transactionId
) ||
425 chargingProfile
.transactionId
== null
428 resetAuthorizeConnectorStatus(connectorStatus
)
429 connectorStatus
.transactionRemoteStarted
= false
430 connectorStatus
.transactionStarted
= false
431 delete connectorStatus
.transactionStart
432 delete connectorStatus
.transactionId
433 delete connectorStatus
.transactionIdTag
434 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
435 delete connectorStatus
.transactionBeginMeterValue
438 export const createBootNotificationRequest
= (
439 stationInfo
: ChargingStationInfo
,
440 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
441 ): BootNotificationRequest
| undefined => {
442 const ocppVersion
= stationInfo
.ocppVersion
443 switch (ocppVersion
) {
444 case OCPPVersion
.VERSION_16
:
446 chargePointModel
: stationInfo
.chargePointModel
,
447 chargePointVendor
: stationInfo
.chargePointVendor
,
448 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
449 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
451 ...(stationInfo
.chargePointSerialNumber
!= null && {
452 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
454 ...(stationInfo
.firmwareVersion
!= null && {
455 firmwareVersion
: stationInfo
.firmwareVersion
457 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
458 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
459 ...(stationInfo
.meterSerialNumber
!= null && {
460 meterSerialNumber
: stationInfo
.meterSerialNumber
462 ...(stationInfo
.meterType
!= null && {
463 meterType
: stationInfo
.meterType
465 } satisfies OCPP16BootNotificationRequest
466 case OCPPVersion
.VERSION_20
:
467 case OCPPVersion
.VERSION_201
:
471 model
: stationInfo
.chargePointModel
,
472 vendorName
: stationInfo
.chargePointVendor
,
473 ...(stationInfo
.firmwareVersion
!= null && {
474 firmwareVersion
: stationInfo
.firmwareVersion
476 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
477 serialNumber
: stationInfo
.chargeBoxSerialNumber
479 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
481 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
482 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
486 } satisfies OCPP20BootNotificationRequest
490 export const warnTemplateKeysDeprecation
= (
491 stationTemplate
: ChargingStationTemplate
,
495 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
496 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
497 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
498 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
499 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
501 for (const templateKey
of templateKeys
) {
502 warnDeprecatedTemplateKey(
504 templateKey
.deprecatedKey
,
507 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
509 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
513 export const stationTemplateToStationInfo
= (
514 stationTemplate
: ChargingStationTemplate
515 ): ChargingStationInfo
=> {
516 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
517 delete stationTemplate
.power
518 delete stationTemplate
.powerUnit
519 delete stationTemplate
.Connectors
520 delete stationTemplate
.Evses
521 delete stationTemplate
.Configuration
522 delete stationTemplate
.AutomaticTransactionGenerator
523 delete stationTemplate
.numberOfConnectors
524 delete stationTemplate
.chargeBoxSerialNumberPrefix
525 delete stationTemplate
.chargePointSerialNumberPrefix
526 delete stationTemplate
.meterSerialNumberPrefix
527 return stationTemplate
as ChargingStationInfo
530 export const createSerialNumber
= (
531 stationTemplate
: ChargingStationTemplate
,
532 stationInfo
: ChargingStationInfo
,
534 randomSerialNumberUpperCase
?: boolean
535 randomSerialNumber
?: boolean
539 ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true },
542 const serialNumberSuffix
=
543 params
.randomSerialNumber
=== true
544 ? getRandomSerialNumberSuffix({
545 upperCase
: params
.randomSerialNumberUpperCase
548 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
549 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
550 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
551 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
552 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
553 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
556 export const propagateSerialNumber
= (
557 stationTemplate
: ChargingStationTemplate
| undefined,
558 stationInfoSrc
: ChargingStationInfo
| undefined,
559 stationInfoDst
: ChargingStationInfo
561 if (stationInfoSrc
== null || stationTemplate
== null) {
563 'Missing charging station template or existing configuration to propagate serial number'
566 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
567 stationInfoSrc
.chargePointSerialNumber
!= null
568 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
569 : stationInfoDst
.chargePointSerialNumber
!= null &&
570 delete stationInfoDst
.chargePointSerialNumber
571 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
572 stationInfoSrc
.chargeBoxSerialNumber
!= null
573 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
574 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
575 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
576 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
577 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
580 export const hasFeatureProfile
= (
581 chargingStation
: ChargingStation
,
582 featureProfile
: SupportedFeatureProfiles
583 ): boolean | undefined => {
584 return getConfigurationKey(
586 StandardParametersKey
.SupportedFeatureProfiles
587 )?.value
?.includes(featureProfile
)
590 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
592 switch (stationInfo
.amperageLimitationUnit
) {
593 case AmpereUnits
.DECI_AMPERE
:
596 case AmpereUnits
.CENTI_AMPERE
:
599 case AmpereUnits
.MILLI_AMPERE
:
607 * Gets the connector cloned charging profiles applying a power limitation
608 * and sorted by connector id descending then stack level descending
610 * @param chargingStation -
611 * @param connectorId -
612 * @returns connector charging profiles array
614 export const getConnectorChargingProfiles
= (
615 chargingStation
: ChargingStation
,
617 ): ChargingProfile
[] => {
618 return clone
<ChargingProfile
[]>(
619 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
620 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
622 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
623 (a
, b
) => b
.stackLevel
- a
.stackLevel
629 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
630 chargingStation
: ChargingStation
,
632 ): number | undefined => {
633 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
634 // Get charging profiles sorted by connector id then stack level
635 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
636 if (isNotEmptyArray(chargingProfiles
)) {
637 const result
= getLimitFromChargingProfiles(
641 chargingStation
.logPrefix()
643 if (result
!= null) {
645 chargingProfile
= result
.chargingProfile
646 switch (chargingStation
.stationInfo
?.currentOutType
) {
649 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
651 : ACElectricUtils
.powerTotal(
652 chargingStation
.getNumberOfPhases(),
653 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
654 chargingStation
.stationInfo
.voltageOut
!,
660 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
662 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
663 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
665 const connectorMaximumPower
=
666 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
667 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
668 if (limit
> connectorMaximumPower
) {
670 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
671 chargingProfile.chargingProfileId
672 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
675 limit
= connectorMaximumPower
682 export const getDefaultVoltageOut
= (
683 currentType
: CurrentType
,
687 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
688 let defaultVoltageOut
: number
689 switch (currentType
) {
691 defaultVoltageOut
= Voltage
.VOLTAGE_230
694 defaultVoltageOut
= Voltage
.VOLTAGE_400
697 logger
.error(`${logPrefix} ${errorMsg}`)
698 throw new BaseError(errorMsg
)
700 return defaultVoltageOut
703 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
704 return stationInfo
.idTagsFile
!= null
705 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
709 export const waitChargingStationEvents
= async (
710 emitter
: EventEmitter
,
711 event
: ChargingStationWorkerMessageEvents
,
713 ): Promise
<number> => {
714 return await new Promise
<number>(resolve
=> {
716 if (eventsToWait
=== 0) {
720 emitter
.on(event
, () => {
722 if (events
=== eventsToWait
) {
729 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
730 let configuredMaxNumberOfConnectors
= 0
731 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
732 const numberOfConnectors
= stationTemplate
.numberOfConnectors
733 configuredMaxNumberOfConnectors
=
734 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
735 } else if (stationTemplate
.numberOfConnectors
!= null) {
736 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
737 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
738 configuredMaxNumberOfConnectors
=
739 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
740 stationTemplate
.Connectors
[0] != null
741 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
742 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
743 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
744 for (const evse
in stationTemplate
.Evses
) {
748 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
749 stationTemplate
.Evses
[evse
].Connectors
753 return configuredMaxNumberOfConnectors
756 const checkConfiguredMaxConnectors
= (
757 configuredMaxConnectors
: number,
761 if (configuredMaxConnectors
<= 0) {
763 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
768 const checkTemplateMaxConnectors
= (
769 templateMaxConnectors
: number,
773 if (templateMaxConnectors
=== 0) {
775 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
777 } else if (templateMaxConnectors
< 0) {
779 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
784 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
785 connectorStatus
.availability
= AvailabilityType
.Operative
786 connectorStatus
.idTagLocalAuthorized
= false
787 connectorStatus
.idTagAuthorized
= false
788 connectorStatus
.transactionRemoteStarted
= false
789 connectorStatus
.transactionStarted
= false
790 connectorStatus
.energyActiveImportRegisterValue
= 0
791 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
792 if (connectorStatus
.chargingProfiles
== null) {
793 connectorStatus
.chargingProfiles
= []
797 const warnDeprecatedTemplateKey
= (
798 template
: ChargingStationTemplate
,
801 templateFile
: string,
804 if (template
[key
as keyof ChargingStationTemplate
] != null) {
805 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
806 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
808 logger
.warn(`${logPrefix} ${logMsg}`)
809 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
813 const convertDeprecatedTemplateKey
= (
814 template
: ChargingStationTemplate
,
815 deprecatedKey
: string,
818 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
820 (template
as unknown
as Record
<string, unknown
>)[key
] =
821 template
[deprecatedKey
as keyof ChargingStationTemplate
]
823 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
824 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
828 interface ChargingProfilesLimit
{
830 chargingProfile
: ChargingProfile
834 * Charging profiles shall already be sorted by connector id descending then stack level descending
836 * @param chargingStation -
837 * @param connectorId -
838 * @param chargingProfiles -
840 * @returns ChargingProfilesLimit
842 const getLimitFromChargingProfiles
= (
843 chargingStation
: ChargingStation
,
845 chargingProfiles
: ChargingProfile
[],
847 ): ChargingProfilesLimit
| undefined => {
848 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
849 const currentDate
= new Date()
850 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
851 for (const chargingProfile
of chargingProfiles
) {
852 const chargingSchedule
= chargingProfile
.chargingSchedule
853 if (chargingSchedule
.startSchedule
== null) {
855 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
857 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
858 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
860 if (!isDate(chargingSchedule
.startSchedule
)) {
862 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
864 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
865 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
867 if (chargingSchedule
.duration
== null) {
869 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
871 // OCPP specifies that if duration is not defined, it should be infinite
872 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
874 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
877 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
880 // Check if the charging profile is active
882 isWithinInterval(currentDate
, {
883 start
: chargingSchedule
.startSchedule
,
884 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
887 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
888 const chargingSchedulePeriodCompareFn
= (
889 a
: ChargingSchedulePeriod
,
890 b
: ChargingSchedulePeriod
891 ): number => a
.startPeriod
- b
.startPeriod
893 !isArraySorted
<ChargingSchedulePeriod
>(
894 chargingSchedule
.chargingSchedulePeriod
,
895 chargingSchedulePeriodCompareFn
899 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
901 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
903 // Check if the first schedule period startPeriod property is equal to 0
904 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
906 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
910 // Handle only one schedule period
911 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
912 const result
: ChargingProfilesLimit
= {
913 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
916 logger
.debug(debugLogMsg
, result
)
919 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
920 // Search for the right schedule period
923 chargingSchedulePeriod
924 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
925 // Find the right schedule period
928 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
932 // Found the schedule period: previous is the correct one
933 const result
: ChargingProfilesLimit
= {
934 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
935 limit
: previousChargingSchedulePeriod
!.limit
,
938 logger
.debug(debugLogMsg
, result
)
941 // Keep a reference to previous one
942 previousChargingSchedulePeriod
= chargingSchedulePeriod
943 // Handle the last schedule period within the charging profile duration
945 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
946 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
949 chargingSchedule
.startSchedule
,
950 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
952 chargingSchedule
.startSchedule
953 ) > chargingSchedule
.duration
)
955 const result
: ChargingProfilesLimit
= {
956 limit
: previousChargingSchedulePeriod
.limit
,
959 logger
.debug(debugLogMsg
, result
)
968 export const prepareChargingProfileKind
= (
969 connectorStatus
: ConnectorStatus
| undefined,
970 chargingProfile
: ChargingProfile
,
971 currentDate
: string | number | Date,
974 switch (chargingProfile
.chargingProfileKind
) {
975 case ChargingProfileKindType
.RECURRING
:
976 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
979 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
981 case ChargingProfileKindType
.RELATIVE
:
982 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
984 `${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`
986 delete chargingProfile
.chargingSchedule
.startSchedule
988 if (connectorStatus
?.transactionStarted
=== true) {
989 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
991 // FIXME: handle relative charging profile duration
997 export const canProceedChargingProfile
= (
998 chargingProfile
: ChargingProfile
,
999 currentDate
: string | number | Date,
1003 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1004 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1007 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1008 chargingProfile.chargingProfileId
1009 } is not valid for the current date ${
1010 isDate(currentDate) ? currentDate.toISOString() : currentDate
1016 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1017 chargingProfile
.chargingSchedule
.duration
== null
1020 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1024 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1026 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1030 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1032 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1039 const canProceedRecurringChargingProfile
= (
1040 chargingProfile
: ChargingProfile
,
1044 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1045 chargingProfile
.recurrencyKind
== null
1048 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1053 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1054 chargingProfile
.chargingSchedule
.startSchedule
== null
1057 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1065 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1067 * @param chargingProfile -
1068 * @param currentDate -
1069 * @param logPrefix -
1071 const prepareRecurringChargingProfile
= (
1072 chargingProfile
: ChargingProfile
,
1073 currentDate
: string | number | Date,
1076 const chargingSchedule
= chargingProfile
.chargingSchedule
1077 let recurringIntervalTranslated
= false
1078 let recurringInterval
: Interval
| undefined
1079 switch (chargingProfile
.recurrencyKind
) {
1080 case RecurrencyKindType
.DAILY
:
1081 recurringInterval
= {
1082 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1083 start
: chargingSchedule
.startSchedule
!,
1084 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1085 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1087 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1089 !isWithinInterval(currentDate
, recurringInterval
) &&
1090 isBefore(recurringInterval
.end
, currentDate
)
1092 chargingSchedule
.startSchedule
= addDays(
1093 recurringInterval
.start
,
1094 differenceInDays(currentDate
, recurringInterval
.start
)
1096 recurringInterval
= {
1097 start
: chargingSchedule
.startSchedule
,
1098 end
: addDays(chargingSchedule
.startSchedule
, 1)
1100 recurringIntervalTranslated
= true
1103 case RecurrencyKindType
.WEEKLY
:
1104 recurringInterval
= {
1105 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1106 start
: chargingSchedule
.startSchedule
!,
1107 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1108 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1110 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1112 !isWithinInterval(currentDate
, recurringInterval
) &&
1113 isBefore(recurringInterval
.end
, currentDate
)
1115 chargingSchedule
.startSchedule
= addWeeks(
1116 recurringInterval
.start
,
1117 differenceInWeeks(currentDate
, recurringInterval
.start
)
1119 recurringInterval
= {
1120 start
: chargingSchedule
.startSchedule
,
1121 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1123 recurringIntervalTranslated
= true
1128 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1131 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1132 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1134 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1135 chargingProfile.recurrencyKind
1136 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1137 recurringInterval?.start as Date
1138 ).toISOString()}, ${toDate(
1139 recurringInterval?.end as Date
1140 ).toISOString()}] has not been properly translated to current date ${
1141 isDate(currentDate) ? currentDate.toISOString() : currentDate
1145 return recurringIntervalTranslated
1148 const checkRecurringChargingProfileDuration
= (
1149 chargingProfile
: ChargingProfile
,
1153 if (chargingProfile
.chargingSchedule
.duration
== null) {
1155 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1156 chargingProfile.chargingProfileKind
1157 } charging profile id ${
1158 chargingProfile.chargingProfileId
1159 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1164 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1166 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1169 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1170 chargingProfile.chargingProfileKind
1171 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1172 chargingProfile.chargingSchedule.duration
1173 } is greater than the recurrency time interval duration ${differenceInSeconds(
1178 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1182 const getRandomSerialNumberSuffix
= (params
?: {
1183 randomBytesLength
?: number
1186 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1187 if (params
?.upperCase
=== true) {
1188 return randomSerialNumberSuffix
.toUpperCase()
1190 return randomSerialNumberSuffix