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
,
34 ChargingProfilePurposeType
,
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'
70 } from
'../utils/index.js'
71 import type { ChargingStation
} from
'./ChargingStation.js'
72 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
74 const moduleName
= 'Helpers'
76 export const buildTemplateName
= (templateFile
: string): string => {
77 if (isAbsolute(templateFile
)) {
78 templateFile
= relative(
79 resolve(join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'station-templates')),
83 const templateFileParsedPath
= parse(templateFile
)
84 return join(templateFileParsedPath
.dir
, templateFileParsedPath
.name
)
87 export const getChargingStationId
= (
89 stationTemplate
: ChargingStationTemplate
| undefined
91 if (stationTemplate
== null) {
92 return "Unknown 'chargingStationId'"
94 // In case of multiple instances: add instance index to charging station id
95 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
96 const idSuffix
= stationTemplate
.nameSuffix
?? ''
97 const idStr
= `000000000${index.toString()}`
98 return stationTemplate
.fixedName
=== true
99 ? stationTemplate
.baseName
100 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
105 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
106 return isPast(reservation
.expiryDate
)
109 export const removeExpiredReservations
= async (
110 chargingStation
: ChargingStation
111 ): Promise
<void> => {
112 if (chargingStation
.hasEvses
) {
113 for (const evseStatus
of chargingStation
.evses
.values()) {
114 for (const connectorStatus
of evseStatus
.connectors
.values()) {
116 connectorStatus
.reservation
!= null &&
117 hasReservationExpired(connectorStatus
.reservation
)
119 await chargingStation
.removeReservation(
120 connectorStatus
.reservation
,
121 ReservationTerminationReason
.EXPIRED
127 for (const connectorStatus
of chargingStation
.connectors
.values()) {
129 connectorStatus
.reservation
!= null &&
130 hasReservationExpired(connectorStatus
.reservation
)
132 await chargingStation
.removeReservation(
133 connectorStatus
.reservation
,
134 ReservationTerminationReason
.EXPIRED
141 export const getNumberOfReservableConnectors
= (
142 connectors
: Map
<number, ConnectorStatus
>
144 let numberOfReservableConnectors
= 0
145 for (const [connectorId
, connectorStatus
] of connectors
) {
146 if (connectorId
=== 0) {
149 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
150 ++numberOfReservableConnectors
153 return numberOfReservableConnectors
156 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
157 const chargingStationInfo
= {
158 chargePointModel
: stationTemplate
.chargePointModel
,
159 chargePointVendor
: stationTemplate
.chargePointVendor
,
160 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!= null && {
161 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
163 ...(stationTemplate
.chargePointSerialNumberPrefix
!= null && {
164 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
166 ...(stationTemplate
.meterSerialNumberPrefix
!= null && {
167 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
169 ...(stationTemplate
.meterType
!= null && {
170 meterType
: stationTemplate
.meterType
173 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
174 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
178 export const checkChargingStation
= (
179 chargingStation
: ChargingStation
,
182 if (!chargingStation
.started
&& !chargingStation
.starting
) {
183 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
189 export const getPhaseRotationValue
= (
191 numberOfPhases
: number
192 ): string | undefined => {
194 if (connectorId
=== 0 && numberOfPhases
=== 0) {
195 return `${connectorId}.${ConnectorPhaseRotation.RST}`
196 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
197 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
199 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
200 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
201 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
202 return `${connectorId}.${ConnectorPhaseRotation.RST}`
206 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
210 return Object.keys(evses
).length
213 const getMaxNumberOfConnectors
= (
214 connectors
: Record
<string, ConnectorStatus
> | undefined
216 if (connectors
== null) {
219 return Object.keys(connectors
).length
222 export const getBootConnectorStatus
= (
223 chargingStation
: ChargingStation
,
225 connectorStatus
: ConnectorStatus
226 ): ConnectorStatusEnum
=> {
227 let connectorBootStatus
: ConnectorStatusEnum
229 connectorStatus
.status == null &&
230 (!chargingStation
.isChargingStationAvailable() ||
231 !chargingStation
.isConnectorAvailable(connectorId
))
233 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
234 } else if (connectorStatus
.status == null && connectorStatus
.bootStatus
!= null) {
235 // Set boot status in template at startup
236 connectorBootStatus
= connectorStatus
.bootStatus
237 } else if (connectorStatus
.status != null) {
238 // Set previous status at startup
239 connectorBootStatus
= connectorStatus
.status
241 // Set default status
242 connectorBootStatus
= ConnectorStatusEnum
.Available
244 return connectorBootStatus
247 export const checkTemplate
= (
248 stationTemplate
: ChargingStationTemplate
| undefined,
252 if (stationTemplate
== null) {
253 const errorMsg
= `Failed to read charging station template file ${templateFile}`
254 logger
.error(`${logPrefix} ${errorMsg}`)
255 throw new BaseError(errorMsg
)
257 if (isEmpty(stationTemplate
)) {
258 const errorMsg
= `Empty charging station information from template file ${templateFile}`
259 logger
.error(`${logPrefix} ${errorMsg}`)
260 throw new BaseError(errorMsg
)
262 if (stationTemplate
.idTagsFile
== null || isEmpty(stationTemplate
.idTagsFile
)) {
264 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
269 export const checkConfiguration
= (
270 stationConfiguration
: ChargingStationConfiguration
| undefined,
272 configurationFile
: string
274 if (stationConfiguration
== null) {
275 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
276 logger
.error(`${logPrefix} ${errorMsg}`)
277 throw new BaseError(errorMsg
)
279 if (isEmpty(stationConfiguration
)) {
280 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
281 logger
.error(`${logPrefix} ${errorMsg}`)
282 throw new BaseError(errorMsg
)
286 export const checkConnectorsConfiguration
= (
287 stationTemplate
: ChargingStationTemplate
,
291 configuredMaxConnectors
: number
292 templateMaxConnectors
: number
293 templateMaxAvailableConnectors
: number
295 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
296 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
297 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
298 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
299 const templateMaxAvailableConnectors
=
300 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
302 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
303 stationTemplate
.randomConnectors
!== true
306 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
308 stationTemplate
.randomConnectors
= true
311 configuredMaxConnectors
,
312 templateMaxConnectors
,
313 templateMaxAvailableConnectors
317 export const checkStationInfoConnectorStatus
= (
319 connectorStatus
: ConnectorStatus
,
323 if (connectorStatus
.status != null) {
325 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
327 delete connectorStatus
.status
331 export const setChargingStationOptions
= (
332 stationInfo
: ChargingStationInfo
,
333 options
?: ChargingStationOptions
334 ): ChargingStationInfo
=> {
335 if (options
?.supervisionUrls
!= null) {
336 stationInfo
.supervisionUrls
= options
.supervisionUrls
338 if (options
?.persistentConfiguration
!= null) {
339 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
340 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
341 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
342 options
.persistentConfiguration
344 if (options
?.autoStart
!= null) {
345 stationInfo
.autoStart
= options
.autoStart
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 buildConnectorsMap
= (
363 connectors
: Record
<string, ConnectorStatus
>,
366 ): Map
<number, ConnectorStatus
> => {
367 const connectorsMap
= new Map
<number, ConnectorStatus
>()
368 if (getMaxNumberOfConnectors(connectors
) > 0) {
369 for (const connector
in connectors
) {
370 const connectorStatus
= connectors
[connector
]
371 const connectorId
= convertToInt(connector
)
372 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
373 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
377 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
383 export const initializeConnectorsMapStatus
= (
384 connectors
: Map
<number, ConnectorStatus
>,
387 for (const connectorId
of connectors
.keys()) {
388 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
390 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
391 connectors.get(connectorId)?.transactionId
395 if (connectorId
=== 0) {
396 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
397 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
398 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 connectors
.get(connectorId
)!.chargingProfiles
= []
402 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
403 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
404 initializeConnectorStatus(connectors
.get(connectorId
)!)
409 export const resetAuthorizeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
410 connectorStatus
.idTagLocalAuthorized
= false
411 connectorStatus
.idTagAuthorized
= false
412 delete connectorStatus
.localAuthorizeIdTag
413 delete connectorStatus
.authorizeIdTag
416 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
417 if (connectorStatus
== null) {
420 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
421 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
.filter(
423 chargingProfile
.chargingProfilePurpose
!== ChargingProfilePurposeType
.TX_PROFILE
||
424 (chargingProfile
.transactionId
!= null &&
425 connectorStatus
.transactionId
!= null &&
426 chargingProfile
.transactionId
!== connectorStatus
.transactionId
)
429 resetAuthorizeConnectorStatus(connectorStatus
)
430 connectorStatus
.transactionRemoteStarted
= false
431 connectorStatus
.transactionStarted
= false
432 delete connectorStatus
.transactionStart
433 delete connectorStatus
.transactionId
434 delete connectorStatus
.transactionIdTag
435 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
436 delete connectorStatus
.transactionBeginMeterValue
439 export const prepareDatesInConnectorStatus
= (
440 connectorStatus
: ConnectorStatus
441 ): ConnectorStatus
=> {
442 if (connectorStatus
.reservation
!= null) {
443 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
444 connectorStatus
.reservation
.expiryDate
= convertToDate(connectorStatus
.reservation
.expiryDate
)!
446 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
447 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
.map(chargingProfile
=> {
448 chargingProfile
.chargingSchedule
.startSchedule
= convertToDate(
449 chargingProfile
.chargingSchedule
.startSchedule
451 chargingProfile
.validFrom
= convertToDate(chargingProfile
.validFrom
)
452 chargingProfile
.validTo
= convertToDate(chargingProfile
.validTo
)
453 return chargingProfile
456 return connectorStatus
459 export const createBootNotificationRequest
= (
460 stationInfo
: ChargingStationInfo
,
461 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
462 ): BootNotificationRequest
| undefined => {
463 const ocppVersion
= stationInfo
.ocppVersion
464 switch (ocppVersion
) {
465 case OCPPVersion
.VERSION_16
:
467 chargePointModel
: stationInfo
.chargePointModel
,
468 chargePointVendor
: stationInfo
.chargePointVendor
,
469 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
470 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
472 ...(stationInfo
.chargePointSerialNumber
!= null && {
473 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
475 ...(stationInfo
.firmwareVersion
!= null && {
476 firmwareVersion
: stationInfo
.firmwareVersion
478 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
479 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
480 ...(stationInfo
.meterSerialNumber
!= null && {
481 meterSerialNumber
: stationInfo
.meterSerialNumber
483 ...(stationInfo
.meterType
!= null && {
484 meterType
: stationInfo
.meterType
486 } satisfies OCPP16BootNotificationRequest
487 case OCPPVersion
.VERSION_20
:
488 case OCPPVersion
.VERSION_201
:
492 model
: stationInfo
.chargePointModel
,
493 vendorName
: stationInfo
.chargePointVendor
,
494 ...(stationInfo
.firmwareVersion
!= null && {
495 firmwareVersion
: stationInfo
.firmwareVersion
497 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
498 serialNumber
: stationInfo
.chargeBoxSerialNumber
500 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
502 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
503 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
507 } satisfies OCPP20BootNotificationRequest
511 export const warnTemplateKeysDeprecation
= (
512 stationTemplate
: ChargingStationTemplate
,
516 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
517 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
518 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
519 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
520 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
522 for (const templateKey
of templateKeys
) {
523 warnDeprecatedTemplateKey(
525 templateKey
.deprecatedKey
,
528 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
530 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
534 export const stationTemplateToStationInfo
= (
535 stationTemplate
: ChargingStationTemplate
536 ): ChargingStationInfo
=> {
537 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
538 delete stationTemplate
.power
539 delete stationTemplate
.powerUnit
540 delete stationTemplate
.Connectors
541 delete stationTemplate
.Evses
542 delete stationTemplate
.Configuration
543 delete stationTemplate
.AutomaticTransactionGenerator
544 delete stationTemplate
.numberOfConnectors
545 delete stationTemplate
.chargeBoxSerialNumberPrefix
546 delete stationTemplate
.chargePointSerialNumberPrefix
547 delete stationTemplate
.meterSerialNumberPrefix
548 return stationTemplate
as ChargingStationInfo
551 export const createSerialNumber
= (
552 stationTemplate
: ChargingStationTemplate
,
553 stationInfo
: ChargingStationInfo
,
555 randomSerialNumberUpperCase
?: boolean
556 randomSerialNumber
?: boolean
560 ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true },
563 const serialNumberSuffix
=
564 params
.randomSerialNumber
=== true
565 ? getRandomSerialNumberSuffix({
566 upperCase
: params
.randomSerialNumberUpperCase
569 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
570 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
571 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
572 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
573 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
574 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
577 export const propagateSerialNumber
= (
578 stationTemplate
: ChargingStationTemplate
| undefined,
579 stationInfoSrc
: ChargingStationInfo
| undefined,
580 stationInfoDst
: ChargingStationInfo
582 if (stationInfoSrc
== null || stationTemplate
== null) {
584 'Missing charging station template or existing configuration to propagate serial number'
587 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
588 stationInfoSrc
.chargePointSerialNumber
!= null
589 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
590 : stationInfoDst
.chargePointSerialNumber
!= null &&
591 delete stationInfoDst
.chargePointSerialNumber
592 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
593 stationInfoSrc
.chargeBoxSerialNumber
!= null
594 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
595 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
596 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
597 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
598 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
601 export const hasFeatureProfile
= (
602 chargingStation
: ChargingStation
,
603 featureProfile
: SupportedFeatureProfiles
604 ): boolean | undefined => {
605 return getConfigurationKey(
607 StandardParametersKey
.SupportedFeatureProfiles
608 )?.value
?.includes(featureProfile
)
611 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
613 switch (stationInfo
.amperageLimitationUnit
) {
614 case AmpereUnits
.DECI_AMPERE
:
617 case AmpereUnits
.CENTI_AMPERE
:
620 case AmpereUnits
.MILLI_AMPERE
:
628 * Gets the connector cloned charging profiles applying a power limitation
629 * and sorted by connector id descending then stack level descending
631 * @param chargingStation -
632 * @param connectorId -
633 * @returns connector charging profiles array
635 export const getConnectorChargingProfiles
= (
636 chargingStation
: ChargingStation
,
638 ): ChargingProfile
[] => {
639 return (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
641 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
643 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? [])
645 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
649 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
650 chargingStation
: ChargingStation
,
652 ): number | undefined => {
653 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
654 // Get charging profiles sorted by connector id then stack level
655 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
656 if (isNotEmptyArray(chargingProfiles
)) {
657 const result
= getLimitFromChargingProfiles(
661 chargingStation
.logPrefix()
663 if (result
!= null) {
665 chargingProfile
= result
.chargingProfile
666 switch (chargingStation
.stationInfo
?.currentOutType
) {
669 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
671 : ACElectricUtils
.powerTotal(
672 chargingStation
.getNumberOfPhases(),
673 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
674 chargingStation
.stationInfo
.voltageOut
!,
680 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
682 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
683 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
685 const connectorMaximumPower
=
686 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
687 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
688 if (limit
> connectorMaximumPower
) {
690 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
691 chargingProfile.chargingProfileId
692 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
695 limit
= connectorMaximumPower
702 export const getDefaultVoltageOut
= (
703 currentType
: CurrentType
,
707 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
708 let defaultVoltageOut
: number
709 switch (currentType
) {
711 defaultVoltageOut
= Voltage
.VOLTAGE_230
714 defaultVoltageOut
= Voltage
.VOLTAGE_400
717 logger
.error(`${logPrefix} ${errorMsg}`)
718 throw new BaseError(errorMsg
)
720 return defaultVoltageOut
723 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
724 return stationInfo
.idTagsFile
!= null
725 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
729 export const waitChargingStationEvents
= async (
730 emitter
: EventEmitter
,
731 event
: ChargingStationWorkerMessageEvents
,
733 ): Promise
<number> => {
734 return await new Promise
<number>(resolve
=> {
736 if (eventsToWait
=== 0) {
740 emitter
.on(event
, () => {
742 if (events
=== eventsToWait
) {
749 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
750 let configuredMaxNumberOfConnectors
= 0
751 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
752 const numberOfConnectors
= stationTemplate
.numberOfConnectors
753 configuredMaxNumberOfConnectors
=
754 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
755 } else if (stationTemplate
.numberOfConnectors
!= null) {
756 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
757 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
758 configuredMaxNumberOfConnectors
=
759 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
760 stationTemplate
.Connectors
[0] != null
761 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
762 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
763 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
764 for (const evse
in stationTemplate
.Evses
) {
768 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
769 stationTemplate
.Evses
[evse
].Connectors
773 return configuredMaxNumberOfConnectors
776 const checkConfiguredMaxConnectors
= (
777 configuredMaxConnectors
: number,
781 if (configuredMaxConnectors
<= 0) {
783 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
788 const checkTemplateMaxConnectors
= (
789 templateMaxConnectors
: number,
793 if (templateMaxConnectors
=== 0) {
795 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
797 } else if (templateMaxConnectors
< 0) {
799 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
804 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
805 connectorStatus
.availability
= AvailabilityType
.Operative
806 connectorStatus
.idTagLocalAuthorized
= false
807 connectorStatus
.idTagAuthorized
= false
808 connectorStatus
.transactionRemoteStarted
= false
809 connectorStatus
.transactionStarted
= false
810 connectorStatus
.energyActiveImportRegisterValue
= 0
811 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
812 if (connectorStatus
.chargingProfiles
== null) {
813 connectorStatus
.chargingProfiles
= []
817 const warnDeprecatedTemplateKey
= (
818 template
: ChargingStationTemplate
,
821 templateFile
: string,
824 if (template
[key
as keyof ChargingStationTemplate
] != null) {
825 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
826 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
828 logger
.warn(`${logPrefix} ${logMsg}`)
829 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
833 const convertDeprecatedTemplateKey
= (
834 template
: ChargingStationTemplate
,
835 deprecatedKey
: string,
838 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
840 (template
as unknown
as Record
<string, unknown
>)[key
] =
841 template
[deprecatedKey
as keyof ChargingStationTemplate
]
843 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
844 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
848 interface ChargingProfilesLimit
{
850 chargingProfile
: ChargingProfile
854 * Charging profiles shall already be sorted by connector id descending then stack level descending
856 * @param chargingStation -
857 * @param connectorId -
858 * @param chargingProfiles -
860 * @returns ChargingProfilesLimit
862 const getLimitFromChargingProfiles
= (
863 chargingStation
: ChargingStation
,
865 chargingProfiles
: ChargingProfile
[],
867 ): ChargingProfilesLimit
| undefined => {
868 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
869 const currentDate
= new Date()
870 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
871 for (const chargingProfile
of chargingProfiles
) {
872 const chargingSchedule
= chargingProfile
.chargingSchedule
873 if (chargingSchedule
.startSchedule
== null) {
875 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
877 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
878 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
880 if (!isDate(chargingSchedule
.startSchedule
)) {
882 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
884 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
885 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
887 if (chargingSchedule
.duration
== null) {
889 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
891 // OCPP specifies that if duration is not defined, it should be infinite
892 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
894 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
897 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
900 // Check if the charging profile is active
902 isWithinInterval(currentDate
, {
903 start
: chargingSchedule
.startSchedule
,
904 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
907 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
908 const chargingSchedulePeriodCompareFn
= (
909 a
: ChargingSchedulePeriod
,
910 b
: ChargingSchedulePeriod
911 ): number => a
.startPeriod
- b
.startPeriod
913 !isArraySorted
<ChargingSchedulePeriod
>(
914 chargingSchedule
.chargingSchedulePeriod
,
915 chargingSchedulePeriodCompareFn
919 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
921 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
923 // Check if the first schedule period startPeriod property is equal to 0
924 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
926 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
930 // Handle only one schedule period
931 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
932 const result
: ChargingProfilesLimit
= {
933 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
936 logger
.debug(debugLogMsg
, result
)
939 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
940 // Search for the right schedule period
943 chargingSchedulePeriod
944 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
945 // Find the right schedule period
948 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
952 // Found the schedule period: previous is the correct one
953 const result
: ChargingProfilesLimit
= {
954 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
955 limit
: previousChargingSchedulePeriod
!.limit
,
958 logger
.debug(debugLogMsg
, result
)
961 // Keep a reference to previous one
962 previousChargingSchedulePeriod
= chargingSchedulePeriod
963 // Handle the last schedule period within the charging profile duration
965 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
966 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
969 chargingSchedule
.startSchedule
,
970 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
972 chargingSchedule
.startSchedule
973 ) > chargingSchedule
.duration
)
975 const result
: ChargingProfilesLimit
= {
976 limit
: previousChargingSchedulePeriod
.limit
,
979 logger
.debug(debugLogMsg
, result
)
988 export const prepareChargingProfileKind
= (
989 connectorStatus
: ConnectorStatus
| undefined,
990 chargingProfile
: ChargingProfile
,
991 currentDate
: string | number | Date,
994 switch (chargingProfile
.chargingProfileKind
) {
995 case ChargingProfileKindType
.RECURRING
:
996 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
999 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
1001 case ChargingProfileKindType
.RELATIVE
:
1002 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
1004 `${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`
1006 delete chargingProfile
.chargingSchedule
.startSchedule
1008 if (connectorStatus
?.transactionStarted
=== true) {
1009 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
1011 // FIXME: handle relative charging profile duration
1017 export const canProceedChargingProfile
= (
1018 chargingProfile
: ChargingProfile
,
1019 currentDate
: string | number | Date,
1023 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1024 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1027 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1028 chargingProfile.chargingProfileId
1029 } is not valid for the current date ${
1030 isDate(currentDate) ? currentDate.toISOString() : currentDate
1036 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1037 chargingProfile
.chargingSchedule
.duration
== null
1040 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1044 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1046 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1050 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1052 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1059 const canProceedRecurringChargingProfile
= (
1060 chargingProfile
: ChargingProfile
,
1064 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1065 chargingProfile
.recurrencyKind
== null
1068 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1073 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1074 chargingProfile
.chargingSchedule
.startSchedule
== null
1077 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1085 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1087 * @param chargingProfile -
1088 * @param currentDate -
1089 * @param logPrefix -
1091 const prepareRecurringChargingProfile
= (
1092 chargingProfile
: ChargingProfile
,
1093 currentDate
: string | number | Date,
1096 const chargingSchedule
= chargingProfile
.chargingSchedule
1097 let recurringIntervalTranslated
= false
1098 let recurringInterval
: Interval
| undefined
1099 switch (chargingProfile
.recurrencyKind
) {
1100 case RecurrencyKindType
.DAILY
:
1101 recurringInterval
= {
1102 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1103 start
: chargingSchedule
.startSchedule
!,
1104 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1105 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1107 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1109 !isWithinInterval(currentDate
, recurringInterval
) &&
1110 isBefore(recurringInterval
.end
, currentDate
)
1112 chargingSchedule
.startSchedule
= addDays(
1113 recurringInterval
.start
,
1114 differenceInDays(currentDate
, recurringInterval
.start
)
1116 recurringInterval
= {
1117 start
: chargingSchedule
.startSchedule
,
1118 end
: addDays(chargingSchedule
.startSchedule
, 1)
1120 recurringIntervalTranslated
= true
1123 case RecurrencyKindType
.WEEKLY
:
1124 recurringInterval
= {
1125 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1126 start
: chargingSchedule
.startSchedule
!,
1127 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1128 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1130 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1132 !isWithinInterval(currentDate
, recurringInterval
) &&
1133 isBefore(recurringInterval
.end
, currentDate
)
1135 chargingSchedule
.startSchedule
= addWeeks(
1136 recurringInterval
.start
,
1137 differenceInWeeks(currentDate
, recurringInterval
.start
)
1139 recurringInterval
= {
1140 start
: chargingSchedule
.startSchedule
,
1141 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1143 recurringIntervalTranslated
= true
1148 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1151 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1152 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1154 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1155 chargingProfile.recurrencyKind
1156 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1157 recurringInterval?.start as Date
1158 ).toISOString()}, ${toDate(
1159 recurringInterval?.end as Date
1160 ).toISOString()}] has not been properly translated to current date ${
1161 isDate(currentDate) ? currentDate.toISOString() : currentDate
1165 return recurringIntervalTranslated
1168 const checkRecurringChargingProfileDuration
= (
1169 chargingProfile
: ChargingProfile
,
1173 if (chargingProfile
.chargingSchedule
.duration
== null) {
1175 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1176 chargingProfile.chargingProfileKind
1177 } charging profile id ${
1178 chargingProfile.chargingProfileId
1179 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1184 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1186 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1189 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1190 chargingProfile.chargingProfileKind
1191 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1192 chargingProfile.chargingSchedule.duration
1193 } is greater than the recurrency time interval duration ${differenceInSeconds(
1198 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1202 const getRandomSerialNumberSuffix
= (params
?: {
1203 randomBytesLength
?: number
1206 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1207 if (params
?.upperCase
=== true) {
1208 return randomSerialNumberSuffix
.toUpperCase()
1210 return randomSerialNumberSuffix