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
) ||
427 chargingProfile
.chargingProfilePurpose
!== ChargingProfilePurposeType
.TX_PROFILE
430 resetAuthorizeConnectorStatus(connectorStatus
)
431 connectorStatus
.transactionRemoteStarted
= false
432 connectorStatus
.transactionStarted
= false
433 delete connectorStatus
.transactionStart
434 delete connectorStatus
.transactionId
435 delete connectorStatus
.transactionIdTag
436 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
437 delete connectorStatus
.transactionBeginMeterValue
440 export const prepareConnectorStatus
= (connectorStatus
: ConnectorStatus
): ConnectorStatus
=> {
441 if (connectorStatus
.reservation
!= null) {
442 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
443 connectorStatus
.reservation
.expiryDate
= convertToDate(connectorStatus
.reservation
.expiryDate
)!
445 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
446 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
449 chargingProfile
.chargingProfilePurpose
!== ChargingProfilePurposeType
.TX_PROFILE
451 .map(chargingProfile
=> {
452 chargingProfile
.chargingSchedule
.startSchedule
= convertToDate(
453 chargingProfile
.chargingSchedule
.startSchedule
455 chargingProfile
.validFrom
= convertToDate(chargingProfile
.validFrom
)
456 chargingProfile
.validTo
= convertToDate(chargingProfile
.validTo
)
457 return chargingProfile
460 return connectorStatus
463 export const createBootNotificationRequest
= (
464 stationInfo
: ChargingStationInfo
,
465 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
466 ): BootNotificationRequest
| undefined => {
467 const ocppVersion
= stationInfo
.ocppVersion
468 switch (ocppVersion
) {
469 case OCPPVersion
.VERSION_16
:
471 chargePointModel
: stationInfo
.chargePointModel
,
472 chargePointVendor
: stationInfo
.chargePointVendor
,
473 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
474 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
476 ...(stationInfo
.chargePointSerialNumber
!= null && {
477 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
479 ...(stationInfo
.firmwareVersion
!= null && {
480 firmwareVersion
: stationInfo
.firmwareVersion
482 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
483 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
484 ...(stationInfo
.meterSerialNumber
!= null && {
485 meterSerialNumber
: stationInfo
.meterSerialNumber
487 ...(stationInfo
.meterType
!= null && {
488 meterType
: stationInfo
.meterType
490 } satisfies OCPP16BootNotificationRequest
491 case OCPPVersion
.VERSION_20
:
492 case OCPPVersion
.VERSION_201
:
496 model
: stationInfo
.chargePointModel
,
497 vendorName
: stationInfo
.chargePointVendor
,
498 ...(stationInfo
.firmwareVersion
!= null && {
499 firmwareVersion
: stationInfo
.firmwareVersion
501 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
502 serialNumber
: stationInfo
.chargeBoxSerialNumber
504 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
506 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
507 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
511 } satisfies OCPP20BootNotificationRequest
515 export const warnTemplateKeysDeprecation
= (
516 stationTemplate
: ChargingStationTemplate
,
520 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
521 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
522 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
523 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
524 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
526 for (const templateKey
of templateKeys
) {
527 warnDeprecatedTemplateKey(
529 templateKey
.deprecatedKey
,
532 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
534 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
538 export const stationTemplateToStationInfo
= (
539 stationTemplate
: ChargingStationTemplate
540 ): ChargingStationInfo
=> {
541 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
542 delete stationTemplate
.power
543 delete stationTemplate
.powerUnit
544 delete stationTemplate
.Connectors
545 delete stationTemplate
.Evses
546 delete stationTemplate
.Configuration
547 delete stationTemplate
.AutomaticTransactionGenerator
548 delete stationTemplate
.numberOfConnectors
549 delete stationTemplate
.chargeBoxSerialNumberPrefix
550 delete stationTemplate
.chargePointSerialNumberPrefix
551 delete stationTemplate
.meterSerialNumberPrefix
552 return stationTemplate
as ChargingStationInfo
555 export const createSerialNumber
= (
556 stationTemplate
: ChargingStationTemplate
,
557 stationInfo
: ChargingStationInfo
,
559 randomSerialNumberUpperCase
?: boolean
560 randomSerialNumber
?: boolean
564 ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true },
567 const serialNumberSuffix
=
568 params
.randomSerialNumber
=== true
569 ? getRandomSerialNumberSuffix({
570 upperCase
: params
.randomSerialNumberUpperCase
573 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
574 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
575 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
576 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
577 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
578 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
581 export const propagateSerialNumber
= (
582 stationTemplate
: ChargingStationTemplate
| undefined,
583 stationInfoSrc
: ChargingStationInfo
| undefined,
584 stationInfoDst
: ChargingStationInfo
586 if (stationInfoSrc
== null || stationTemplate
== null) {
588 'Missing charging station template or existing configuration to propagate serial number'
591 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
592 stationInfoSrc
.chargePointSerialNumber
!= null
593 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
594 : stationInfoDst
.chargePointSerialNumber
!= null &&
595 delete stationInfoDst
.chargePointSerialNumber
596 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
597 stationInfoSrc
.chargeBoxSerialNumber
!= null
598 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
599 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
600 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
601 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
602 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
605 export const hasFeatureProfile
= (
606 chargingStation
: ChargingStation
,
607 featureProfile
: SupportedFeatureProfiles
608 ): boolean | undefined => {
609 return getConfigurationKey(
611 StandardParametersKey
.SupportedFeatureProfiles
612 )?.value
?.includes(featureProfile
)
615 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
617 switch (stationInfo
.amperageLimitationUnit
) {
618 case AmpereUnits
.DECI_AMPERE
:
621 case AmpereUnits
.CENTI_AMPERE
:
624 case AmpereUnits
.MILLI_AMPERE
:
632 * Gets the connector charging profiles relevant for power limitation shallow cloned and sorted by priorities
634 * @param chargingStation - Charging station
635 * @param connectorId - Connector id
636 * @returns connector charging profiles array
638 export const getConnectorChargingProfiles
= (
639 chargingStation
: ChargingStation
,
641 ): ChargingProfile
[] => {
642 // FIXME: handle charging profile purpose CHARGE_POINT_MAX_PROFILE
643 return (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
647 a
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
&&
648 b
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
652 a
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
&&
653 b
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
657 return b
.stackLevel
- a
.stackLevel
660 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? [])
663 chargingProfile
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
665 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
669 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
670 chargingStation
: ChargingStation
,
672 ): number | undefined => {
673 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
674 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
675 if (isNotEmptyArray(chargingProfiles
)) {
676 const result
= getLimitFromChargingProfiles(
680 chargingStation
.logPrefix()
682 if (result
!= null) {
684 chargingProfile
= result
.chargingProfile
685 switch (chargingStation
.stationInfo
?.currentOutType
) {
688 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
690 : ACElectricUtils
.powerTotal(
691 chargingStation
.getNumberOfPhases(),
692 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
693 chargingStation
.stationInfo
.voltageOut
!,
699 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
701 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
702 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
704 const connectorMaximumPower
=
705 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
706 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
707 if (limit
> connectorMaximumPower
) {
709 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
710 chargingProfile.chargingProfileId
711 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
714 limit
= connectorMaximumPower
721 export const getDefaultVoltageOut
= (
722 currentType
: CurrentType
,
726 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
727 let defaultVoltageOut
: number
728 switch (currentType
) {
730 defaultVoltageOut
= Voltage
.VOLTAGE_230
733 defaultVoltageOut
= Voltage
.VOLTAGE_400
736 logger
.error(`${logPrefix} ${errorMsg}`)
737 throw new BaseError(errorMsg
)
739 return defaultVoltageOut
742 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
743 return stationInfo
.idTagsFile
!= null
744 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
748 export const waitChargingStationEvents
= async (
749 emitter
: EventEmitter
,
750 event
: ChargingStationWorkerMessageEvents
,
752 ): Promise
<number> => {
753 return await new Promise
<number>(resolve
=> {
755 if (eventsToWait
=== 0) {
759 emitter
.on(event
, () => {
761 if (events
=== eventsToWait
) {
768 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
769 let configuredMaxNumberOfConnectors
= 0
770 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
771 const numberOfConnectors
= stationTemplate
.numberOfConnectors
772 configuredMaxNumberOfConnectors
=
773 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
774 } else if (stationTemplate
.numberOfConnectors
!= null) {
775 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
776 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
777 configuredMaxNumberOfConnectors
=
778 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
779 stationTemplate
.Connectors
[0] != null
780 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
781 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
782 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
783 for (const evse
in stationTemplate
.Evses
) {
787 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
788 stationTemplate
.Evses
[evse
].Connectors
792 return configuredMaxNumberOfConnectors
795 const checkConfiguredMaxConnectors
= (
796 configuredMaxConnectors
: number,
800 if (configuredMaxConnectors
<= 0) {
802 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
807 const checkTemplateMaxConnectors
= (
808 templateMaxConnectors
: number,
812 if (templateMaxConnectors
=== 0) {
814 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
816 } else if (templateMaxConnectors
< 0) {
818 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
823 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
824 connectorStatus
.availability
= AvailabilityType
.Operative
825 connectorStatus
.idTagLocalAuthorized
= false
826 connectorStatus
.idTagAuthorized
= false
827 connectorStatus
.transactionRemoteStarted
= false
828 connectorStatus
.transactionStarted
= false
829 connectorStatus
.energyActiveImportRegisterValue
= 0
830 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
831 if (connectorStatus
.chargingProfiles
== null) {
832 connectorStatus
.chargingProfiles
= []
836 const warnDeprecatedTemplateKey
= (
837 template
: ChargingStationTemplate
,
840 templateFile
: string,
843 if (template
[key
as keyof ChargingStationTemplate
] != null) {
844 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
845 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
847 logger
.warn(`${logPrefix} ${logMsg}`)
848 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
852 const convertDeprecatedTemplateKey
= (
853 template
: ChargingStationTemplate
,
854 deprecatedKey
: string,
857 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
859 (template
as unknown
as Record
<string, unknown
>)[key
] =
860 template
[deprecatedKey
as keyof ChargingStationTemplate
]
862 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
863 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
867 interface ChargingProfilesLimit
{
869 chargingProfile
: ChargingProfile
873 * Charging profiles shall already be sorted by connector id descending then stack level descending
875 * @param chargingStation -
876 * @param connectorId -
877 * @param chargingProfiles -
879 * @returns ChargingProfilesLimit
881 const getLimitFromChargingProfiles
= (
882 chargingStation
: ChargingStation
,
884 chargingProfiles
: ChargingProfile
[],
886 ): ChargingProfilesLimit
| undefined => {
887 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
888 const currentDate
= new Date()
889 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
890 let previousActiveChargingProfile
: ChargingProfile
| undefined
891 for (const chargingProfile
of chargingProfiles
) {
892 const chargingSchedule
= chargingProfile
.chargingSchedule
893 if (chargingSchedule
.startSchedule
== null) {
895 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
897 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
898 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
900 if (!isDate(chargingSchedule
.startSchedule
)) {
902 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
904 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
905 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
907 if (chargingSchedule
.duration
== null) {
909 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
911 // OCPP specifies that if duration is not defined, it should be infinite
912 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
914 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
917 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
920 // Check if the charging profile is active
922 isWithinInterval(currentDate
, {
923 start
: chargingSchedule
.startSchedule
,
924 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
927 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
928 const chargingSchedulePeriodCompareFn
= (
929 a
: ChargingSchedulePeriod
,
930 b
: ChargingSchedulePeriod
931 ): number => a
.startPeriod
- b
.startPeriod
933 !isArraySorted
<ChargingSchedulePeriod
>(
934 chargingSchedule
.chargingSchedulePeriod
,
935 chargingSchedulePeriodCompareFn
939 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
941 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
943 // Check if the first schedule period startPeriod property is equal to 0
944 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
946 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
950 // Handle only one schedule period
951 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
952 const result
: ChargingProfilesLimit
= {
953 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
956 logger
.debug(debugLogMsg
, result
)
959 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
960 // Search for the right schedule period
963 chargingSchedulePeriod
964 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
965 // Find the right schedule period
968 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
972 // Found the schedule period: previous is the correct one
973 const result
: ChargingProfilesLimit
= {
974 limit
: previousChargingSchedulePeriod
?.limit
?? chargingSchedulePeriod
.limit
,
975 chargingProfile
: previousActiveChargingProfile
?? chargingProfile
977 logger
.debug(debugLogMsg
, result
)
980 // Handle the last schedule period within the charging profile duration
982 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
983 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
986 chargingSchedule
.startSchedule
,
987 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
989 chargingSchedule
.startSchedule
990 ) > chargingSchedule
.duration
)
992 const result
: ChargingProfilesLimit
= {
993 limit
: chargingSchedulePeriod
.limit
,
996 logger
.debug(debugLogMsg
, result
)
999 // Keep a reference to previous charging schedule period
1000 previousChargingSchedulePeriod
= chargingSchedulePeriod
1003 // Keep a reference to previous active charging profile
1004 previousActiveChargingProfile
= chargingProfile
1009 export const prepareChargingProfileKind
= (
1010 connectorStatus
: ConnectorStatus
| undefined,
1011 chargingProfile
: ChargingProfile
,
1012 currentDate
: string | number | Date,
1015 switch (chargingProfile
.chargingProfileKind
) {
1016 case ChargingProfileKindType
.RECURRING
:
1017 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
1020 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
1022 case ChargingProfileKindType
.RELATIVE
:
1023 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
1025 `${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`
1027 delete chargingProfile
.chargingSchedule
.startSchedule
1029 if (connectorStatus
?.transactionStarted
=== true) {
1030 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
1032 // FIXME: handle relative charging profile duration
1038 export const canProceedChargingProfile
= (
1039 chargingProfile
: ChargingProfile
,
1040 currentDate
: string | number | Date,
1044 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1045 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1048 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1049 chargingProfile.chargingProfileId
1050 } is not valid for the current date ${
1051 isDate(currentDate) ? currentDate.toISOString() : currentDate
1057 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1058 chargingProfile
.chargingSchedule
.duration
== null
1061 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1065 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1067 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1071 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1073 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1080 const canProceedRecurringChargingProfile
= (
1081 chargingProfile
: ChargingProfile
,
1085 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1086 chargingProfile
.recurrencyKind
== null
1089 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1094 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1095 chargingProfile
.chargingSchedule
.startSchedule
== null
1098 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1106 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1108 * @param chargingProfile -
1109 * @param currentDate -
1110 * @param logPrefix -
1112 const prepareRecurringChargingProfile
= (
1113 chargingProfile
: ChargingProfile
,
1114 currentDate
: string | number | Date,
1117 const chargingSchedule
= chargingProfile
.chargingSchedule
1118 let recurringIntervalTranslated
= false
1119 let recurringInterval
: Interval
| undefined
1120 switch (chargingProfile
.recurrencyKind
) {
1121 case RecurrencyKindType
.DAILY
:
1122 recurringInterval
= {
1123 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1124 start
: chargingSchedule
.startSchedule
!,
1125 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1126 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1128 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1130 !isWithinInterval(currentDate
, recurringInterval
) &&
1131 isBefore(recurringInterval
.end
, currentDate
)
1133 chargingSchedule
.startSchedule
= addDays(
1134 recurringInterval
.start
,
1135 differenceInDays(currentDate
, recurringInterval
.start
)
1137 recurringInterval
= {
1138 start
: chargingSchedule
.startSchedule
,
1139 end
: addDays(chargingSchedule
.startSchedule
, 1)
1141 recurringIntervalTranslated
= true
1144 case RecurrencyKindType
.WEEKLY
:
1145 recurringInterval
= {
1146 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1147 start
: chargingSchedule
.startSchedule
!,
1148 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1149 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1151 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1153 !isWithinInterval(currentDate
, recurringInterval
) &&
1154 isBefore(recurringInterval
.end
, currentDate
)
1156 chargingSchedule
.startSchedule
= addWeeks(
1157 recurringInterval
.start
,
1158 differenceInWeeks(currentDate
, recurringInterval
.start
)
1160 recurringInterval
= {
1161 start
: chargingSchedule
.startSchedule
,
1162 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1164 recurringIntervalTranslated
= true
1169 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1172 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1173 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1175 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1176 chargingProfile.recurrencyKind
1177 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1178 recurringInterval?.start as Date
1179 ).toISOString()}, ${toDate(
1180 recurringInterval?.end as Date
1181 ).toISOString()}] has not been properly translated to current date ${
1182 isDate(currentDate) ? currentDate.toISOString() : currentDate
1186 return recurringIntervalTranslated
1189 const checkRecurringChargingProfileDuration
= (
1190 chargingProfile
: ChargingProfile
,
1194 if (chargingProfile
.chargingSchedule
.duration
== null) {
1196 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1197 chargingProfile.chargingProfileKind
1198 } charging profile id ${
1199 chargingProfile.chargingProfileId
1200 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1205 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1207 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1210 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1211 chargingProfile.chargingProfileKind
1212 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1213 chargingProfile.chargingSchedule.duration
1214 } is greater than the recurrency time interval duration ${differenceInSeconds(
1219 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1223 const getRandomSerialNumberSuffix
= (params
?: {
1224 randomBytesLength
?: number
1227 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1228 if (params
?.upperCase
=== true) {
1229 return randomSerialNumberSuffix
.toUpperCase()
1231 return randomSerialNumberSuffix