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 charging profiles relevant for power limitation shallow cloned and sorted by priorities
630 * @param chargingStation - Charging station
631 * @param connectorId - Connector id
632 * @returns connector charging profiles array
634 export const getConnectorChargingProfiles
= (
635 chargingStation
: ChargingStation
,
637 ): ChargingProfile
[] => {
638 // FIXME: handle charging profile purpose CHARGE_POINT_MAX_PROFILE
639 return (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
643 a
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
&&
644 b
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
648 a
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
&&
649 b
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
653 return b
.stackLevel
- a
.stackLevel
656 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? [])
659 chargingProfile
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
661 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
665 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
666 chargingStation
: ChargingStation
,
668 ): number | undefined => {
669 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
670 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
671 if (isNotEmptyArray(chargingProfiles
)) {
672 const result
= getLimitFromChargingProfiles(
676 chargingStation
.logPrefix()
678 if (result
!= null) {
680 chargingProfile
= result
.chargingProfile
681 switch (chargingStation
.stationInfo
?.currentOutType
) {
684 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
686 : ACElectricUtils
.powerTotal(
687 chargingStation
.getNumberOfPhases(),
688 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
689 chargingStation
.stationInfo
.voltageOut
!,
695 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
697 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
698 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
700 const connectorMaximumPower
=
701 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
702 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
703 if (limit
> connectorMaximumPower
) {
705 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
706 chargingProfile.chargingProfileId
707 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
710 limit
= connectorMaximumPower
717 export const getDefaultVoltageOut
= (
718 currentType
: CurrentType
,
722 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
723 let defaultVoltageOut
: number
724 switch (currentType
) {
726 defaultVoltageOut
= Voltage
.VOLTAGE_230
729 defaultVoltageOut
= Voltage
.VOLTAGE_400
732 logger
.error(`${logPrefix} ${errorMsg}`)
733 throw new BaseError(errorMsg
)
735 return defaultVoltageOut
738 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
739 return stationInfo
.idTagsFile
!= null
740 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
744 export const waitChargingStationEvents
= async (
745 emitter
: EventEmitter
,
746 event
: ChargingStationWorkerMessageEvents
,
748 ): Promise
<number> => {
749 return await new Promise
<number>(resolve
=> {
751 if (eventsToWait
=== 0) {
755 emitter
.on(event
, () => {
757 if (events
=== eventsToWait
) {
764 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
765 let configuredMaxNumberOfConnectors
= 0
766 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
767 const numberOfConnectors
= stationTemplate
.numberOfConnectors
768 configuredMaxNumberOfConnectors
=
769 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
770 } else if (stationTemplate
.numberOfConnectors
!= null) {
771 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
772 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
773 configuredMaxNumberOfConnectors
=
774 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
775 stationTemplate
.Connectors
[0] != null
776 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
777 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
778 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
779 for (const evse
in stationTemplate
.Evses
) {
783 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
784 stationTemplate
.Evses
[evse
].Connectors
788 return configuredMaxNumberOfConnectors
791 const checkConfiguredMaxConnectors
= (
792 configuredMaxConnectors
: number,
796 if (configuredMaxConnectors
<= 0) {
798 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
803 const checkTemplateMaxConnectors
= (
804 templateMaxConnectors
: number,
808 if (templateMaxConnectors
=== 0) {
810 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
812 } else if (templateMaxConnectors
< 0) {
814 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
819 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
820 connectorStatus
.availability
= AvailabilityType
.Operative
821 connectorStatus
.idTagLocalAuthorized
= false
822 connectorStatus
.idTagAuthorized
= false
823 connectorStatus
.transactionRemoteStarted
= false
824 connectorStatus
.transactionStarted
= false
825 connectorStatus
.energyActiveImportRegisterValue
= 0
826 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
827 if (connectorStatus
.chargingProfiles
== null) {
828 connectorStatus
.chargingProfiles
= []
832 const warnDeprecatedTemplateKey
= (
833 template
: ChargingStationTemplate
,
836 templateFile
: string,
839 if (template
[key
as keyof ChargingStationTemplate
] != null) {
840 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
841 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
843 logger
.warn(`${logPrefix} ${logMsg}`)
844 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
848 const convertDeprecatedTemplateKey
= (
849 template
: ChargingStationTemplate
,
850 deprecatedKey
: string,
853 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
855 (template
as unknown
as Record
<string, unknown
>)[key
] =
856 template
[deprecatedKey
as keyof ChargingStationTemplate
]
858 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
859 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
863 interface ChargingProfilesLimit
{
865 chargingProfile
: ChargingProfile
869 * Charging profiles shall already be sorted by connector id descending then stack level descending
871 * @param chargingStation -
872 * @param connectorId -
873 * @param chargingProfiles -
875 * @returns ChargingProfilesLimit
877 const getLimitFromChargingProfiles
= (
878 chargingStation
: ChargingStation
,
880 chargingProfiles
: ChargingProfile
[],
882 ): ChargingProfilesLimit
| undefined => {
883 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
884 const currentDate
= new Date()
885 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
886 let previousActiveChargingProfile
: ChargingProfile
| undefined
887 for (const chargingProfile
of chargingProfiles
) {
888 const chargingSchedule
= chargingProfile
.chargingSchedule
889 if (chargingSchedule
.startSchedule
== null) {
891 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
893 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
894 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
896 if (!isDate(chargingSchedule
.startSchedule
)) {
898 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
900 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
901 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
903 if (chargingSchedule
.duration
== null) {
905 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
907 // OCPP specifies that if duration is not defined, it should be infinite
908 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
910 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
913 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
916 // Check if the charging profile is active
918 isWithinInterval(currentDate
, {
919 start
: chargingSchedule
.startSchedule
,
920 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
923 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
924 const chargingSchedulePeriodCompareFn
= (
925 a
: ChargingSchedulePeriod
,
926 b
: ChargingSchedulePeriod
927 ): number => a
.startPeriod
- b
.startPeriod
929 !isArraySorted
<ChargingSchedulePeriod
>(
930 chargingSchedule
.chargingSchedulePeriod
,
931 chargingSchedulePeriodCompareFn
935 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
937 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
939 // Check if the first schedule period startPeriod property is equal to 0
940 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
942 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
946 // Handle only one schedule period
947 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
948 const result
: ChargingProfilesLimit
= {
949 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
952 logger
.debug(debugLogMsg
, result
)
955 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
956 // Search for the right schedule period
959 chargingSchedulePeriod
960 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
961 // Find the right schedule period
964 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
968 // Found the schedule period: previous is the correct one
969 const result
: ChargingProfilesLimit
= {
970 limit
: previousChargingSchedulePeriod
?.limit
?? chargingSchedulePeriod
.limit
,
971 chargingProfile
: previousActiveChargingProfile
?? chargingProfile
973 logger
.debug(debugLogMsg
, result
)
976 // Handle the last schedule period within the charging profile duration
978 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
979 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
982 chargingSchedule
.startSchedule
,
983 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
985 chargingSchedule
.startSchedule
986 ) > chargingSchedule
.duration
)
988 const result
: ChargingProfilesLimit
= {
989 limit
: chargingSchedulePeriod
.limit
,
992 logger
.debug(debugLogMsg
, result
)
995 // Keep a reference to previous charging schedule period
996 previousChargingSchedulePeriod
= chargingSchedulePeriod
999 // Keep a reference to previous active charging profile
1000 previousActiveChargingProfile
= chargingProfile
1005 export const prepareChargingProfileKind
= (
1006 connectorStatus
: ConnectorStatus
| undefined,
1007 chargingProfile
: ChargingProfile
,
1008 currentDate
: string | number | Date,
1011 switch (chargingProfile
.chargingProfileKind
) {
1012 case ChargingProfileKindType
.RECURRING
:
1013 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
1016 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
1018 case ChargingProfileKindType
.RELATIVE
:
1019 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
1021 `${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`
1023 delete chargingProfile
.chargingSchedule
.startSchedule
1025 if (connectorStatus
?.transactionStarted
=== true) {
1026 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
1028 // FIXME: handle relative charging profile duration
1034 export const canProceedChargingProfile
= (
1035 chargingProfile
: ChargingProfile
,
1036 currentDate
: string | number | Date,
1040 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1041 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1044 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1045 chargingProfile.chargingProfileId
1046 } is not valid for the current date ${
1047 isDate(currentDate) ? currentDate.toISOString() : currentDate
1053 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1054 chargingProfile
.chargingSchedule
.duration
== null
1057 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1061 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1063 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1067 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1069 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1076 const canProceedRecurringChargingProfile
= (
1077 chargingProfile
: ChargingProfile
,
1081 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1082 chargingProfile
.recurrencyKind
== null
1085 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1090 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1091 chargingProfile
.chargingSchedule
.startSchedule
== null
1094 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1102 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1104 * @param chargingProfile -
1105 * @param currentDate -
1106 * @param logPrefix -
1108 const prepareRecurringChargingProfile
= (
1109 chargingProfile
: ChargingProfile
,
1110 currentDate
: string | number | Date,
1113 const chargingSchedule
= chargingProfile
.chargingSchedule
1114 let recurringIntervalTranslated
= false
1115 let recurringInterval
: Interval
| undefined
1116 switch (chargingProfile
.recurrencyKind
) {
1117 case RecurrencyKindType
.DAILY
:
1118 recurringInterval
= {
1119 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1120 start
: chargingSchedule
.startSchedule
!,
1121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1122 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1124 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1126 !isWithinInterval(currentDate
, recurringInterval
) &&
1127 isBefore(recurringInterval
.end
, currentDate
)
1129 chargingSchedule
.startSchedule
= addDays(
1130 recurringInterval
.start
,
1131 differenceInDays(currentDate
, recurringInterval
.start
)
1133 recurringInterval
= {
1134 start
: chargingSchedule
.startSchedule
,
1135 end
: addDays(chargingSchedule
.startSchedule
, 1)
1137 recurringIntervalTranslated
= true
1140 case RecurrencyKindType
.WEEKLY
:
1141 recurringInterval
= {
1142 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1143 start
: chargingSchedule
.startSchedule
!,
1144 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1145 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1147 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1149 !isWithinInterval(currentDate
, recurringInterval
) &&
1150 isBefore(recurringInterval
.end
, currentDate
)
1152 chargingSchedule
.startSchedule
= addWeeks(
1153 recurringInterval
.start
,
1154 differenceInWeeks(currentDate
, recurringInterval
.start
)
1156 recurringInterval
= {
1157 start
: chargingSchedule
.startSchedule
,
1158 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1160 recurringIntervalTranslated
= true
1165 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1168 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1169 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1171 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1172 chargingProfile.recurrencyKind
1173 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1174 recurringInterval?.start as Date
1175 ).toISOString()}, ${toDate(
1176 recurringInterval?.end as Date
1177 ).toISOString()}] has not been properly translated to current date ${
1178 isDate(currentDate) ? currentDate.toISOString() : currentDate
1182 return recurringIntervalTranslated
1185 const checkRecurringChargingProfileDuration
= (
1186 chargingProfile
: ChargingProfile
,
1190 if (chargingProfile
.chargingSchedule
.duration
== null) {
1192 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1193 chargingProfile.chargingProfileKind
1194 } charging profile id ${
1195 chargingProfile.chargingProfileId
1196 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1201 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1203 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1206 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1207 chargingProfile.chargingProfileKind
1208 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1209 chargingProfile.chargingSchedule.duration
1210 } is greater than the recurrency time interval duration ${differenceInSeconds(
1215 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1219 const getRandomSerialNumberSuffix
= (params
?: {
1220 randomBytesLength
?: number
1223 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1224 if (params
?.upperCase
=== true) {
1225 return randomSerialNumberSuffix
.toUpperCase()
1227 return randomSerialNumberSuffix