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 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
674 if (isNotEmptyArray(chargingProfiles
)) {
675 const chargingProfilesLimit
= getLimitFromChargingProfiles(
679 chargingStation
.logPrefix()
681 if (chargingProfilesLimit
!= null) {
682 let { limit
, chargingProfile
} = chargingProfilesLimit
683 switch (chargingStation
.stationInfo
?.currentOutType
) {
686 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
688 : ACElectricUtils
.powerTotal(
689 chargingStation
.getNumberOfPhases(),
690 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
691 chargingStation
.stationInfo
.voltageOut
!,
697 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
699 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
700 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
702 const connectorMaximumPower
=
703 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
704 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
705 if (limit
> connectorMaximumPower
) {
707 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
708 chargingProfile.chargingProfileId
709 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
710 chargingProfilesLimit
712 limit
= connectorMaximumPower
719 export const getDefaultVoltageOut
= (
720 currentType
: CurrentType
,
724 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
725 let defaultVoltageOut
: number
726 switch (currentType
) {
728 defaultVoltageOut
= Voltage
.VOLTAGE_230
731 defaultVoltageOut
= Voltage
.VOLTAGE_400
734 logger
.error(`${logPrefix} ${errorMsg}`)
735 throw new BaseError(errorMsg
)
737 return defaultVoltageOut
740 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
741 return stationInfo
.idTagsFile
!= null
742 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
746 export const waitChargingStationEvents
= async (
747 emitter
: EventEmitter
,
748 event
: ChargingStationWorkerMessageEvents
,
750 ): Promise
<number> => {
751 return await new Promise
<number>(resolve
=> {
753 if (eventsToWait
=== 0) {
757 emitter
.on(event
, () => {
759 if (events
=== eventsToWait
) {
766 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
767 let configuredMaxNumberOfConnectors
= 0
768 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
769 const numberOfConnectors
= stationTemplate
.numberOfConnectors
770 configuredMaxNumberOfConnectors
=
771 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
772 } else if (stationTemplate
.numberOfConnectors
!= null) {
773 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
774 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
775 configuredMaxNumberOfConnectors
=
776 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
777 stationTemplate
.Connectors
[0] != null
778 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
779 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
780 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
781 for (const evse
in stationTemplate
.Evses
) {
785 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
786 stationTemplate
.Evses
[evse
].Connectors
790 return configuredMaxNumberOfConnectors
793 const checkConfiguredMaxConnectors
= (
794 configuredMaxConnectors
: number,
798 if (configuredMaxConnectors
<= 0) {
800 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
805 const checkTemplateMaxConnectors
= (
806 templateMaxConnectors
: number,
810 if (templateMaxConnectors
=== 0) {
812 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
814 } else if (templateMaxConnectors
< 0) {
816 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
821 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
822 connectorStatus
.availability
= AvailabilityType
.Operative
823 connectorStatus
.idTagLocalAuthorized
= false
824 connectorStatus
.idTagAuthorized
= false
825 connectorStatus
.transactionRemoteStarted
= false
826 connectorStatus
.transactionStarted
= false
827 connectorStatus
.energyActiveImportRegisterValue
= 0
828 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
829 if (connectorStatus
.chargingProfiles
== null) {
830 connectorStatus
.chargingProfiles
= []
834 const warnDeprecatedTemplateKey
= (
835 template
: ChargingStationTemplate
,
838 templateFile
: string,
841 if (template
[key
as keyof ChargingStationTemplate
] != null) {
842 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
843 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
845 logger
.warn(`${logPrefix} ${logMsg}`)
846 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
850 const convertDeprecatedTemplateKey
= (
851 template
: ChargingStationTemplate
,
852 deprecatedKey
: string,
855 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
857 (template
as unknown
as Record
<string, unknown
>)[key
] =
858 template
[deprecatedKey
as keyof ChargingStationTemplate
]
860 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
861 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
865 interface ChargingProfilesLimit
{
867 chargingProfile
: ChargingProfile
871 * Charging profiles shall already be sorted by connector id descending then stack level descending
873 * @param chargingStation -
874 * @param connectorId -
875 * @param chargingProfiles -
877 * @returns ChargingProfilesLimit
879 const getLimitFromChargingProfiles
= (
880 chargingStation
: ChargingStation
,
882 chargingProfiles
: ChargingProfile
[],
884 ): ChargingProfilesLimit
| undefined => {
885 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profiles limit found: %j`
886 const currentDate
= new Date()
887 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
888 let previousActiveChargingProfile
: ChargingProfile
| undefined
889 for (const chargingProfile
of chargingProfiles
) {
890 const chargingSchedule
= chargingProfile
.chargingSchedule
891 if (chargingSchedule
.startSchedule
== null) {
893 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
895 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
896 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
898 if (!isDate(chargingSchedule
.startSchedule
)) {
900 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
903 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
905 if (chargingSchedule
.duration
== null) {
907 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
909 // OCPP specifies that if duration is not defined, it should be infinite
910 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
912 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
915 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
918 // Check if the charging profile is active
920 isWithinInterval(currentDate
, {
921 start
: chargingSchedule
.startSchedule
,
922 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
925 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
926 const chargingSchedulePeriodCompareFn
= (
927 a
: ChargingSchedulePeriod
,
928 b
: ChargingSchedulePeriod
929 ): number => a
.startPeriod
- b
.startPeriod
931 !isArraySorted
<ChargingSchedulePeriod
>(
932 chargingSchedule
.chargingSchedulePeriod
,
933 chargingSchedulePeriodCompareFn
937 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
939 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
941 // Check if the first schedule period startPeriod property is equal to 0
942 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
944 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
948 // Handle only one schedule period
949 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
950 const chargingProfilesLimit
: ChargingProfilesLimit
= {
951 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
954 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
955 return chargingProfilesLimit
957 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
958 // Search for the right schedule period
961 chargingSchedulePeriod
962 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
963 // Find the right schedule period
966 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
970 // Found the schedule period: previous is the correct one
971 const chargingProfilesLimit
: ChargingProfilesLimit
= {
972 limit
: previousChargingSchedulePeriod
?.limit
?? chargingSchedulePeriod
.limit
,
973 chargingProfile
: previousActiveChargingProfile
?? chargingProfile
975 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
976 return chargingProfilesLimit
978 // Handle the last schedule period within the charging profile duration
980 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
981 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
984 chargingSchedule
.startSchedule
,
985 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
987 chargingSchedule
.startSchedule
988 ) > chargingSchedule
.duration
)
990 const chargingProfilesLimit
: ChargingProfilesLimit
= {
991 limit
: chargingSchedulePeriod
.limit
,
994 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
995 return chargingProfilesLimit
997 // Keep a reference to previous charging schedule period
998 previousChargingSchedulePeriod
= chargingSchedulePeriod
1001 // Keep a reference to previous active charging profile
1002 previousActiveChargingProfile
= chargingProfile
1007 export const prepareChargingProfileKind
= (
1008 connectorStatus
: ConnectorStatus
| undefined,
1009 chargingProfile
: ChargingProfile
,
1010 currentDate
: string | number | Date,
1013 switch (chargingProfile
.chargingProfileKind
) {
1014 case ChargingProfileKindType
.RECURRING
:
1015 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
1018 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
1020 case ChargingProfileKindType
.RELATIVE
:
1021 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
1023 `${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`
1025 delete chargingProfile
.chargingSchedule
.startSchedule
1027 if (connectorStatus
?.transactionStarted
=== true) {
1028 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
1030 // FIXME: handle relative charging profile duration
1036 export const canProceedChargingProfile
= (
1037 chargingProfile
: ChargingProfile
,
1038 currentDate
: string | number | Date,
1042 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1043 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1046 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1047 chargingProfile.chargingProfileId
1048 } is not valid for the current date ${
1049 isDate(currentDate) ? currentDate.toISOString() : currentDate
1055 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1056 chargingProfile
.chargingSchedule
.duration
== null
1059 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1063 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1065 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1069 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1071 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1078 const canProceedRecurringChargingProfile
= (
1079 chargingProfile
: ChargingProfile
,
1083 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1084 chargingProfile
.recurrencyKind
== null
1087 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1092 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1093 chargingProfile
.chargingSchedule
.startSchedule
== null
1096 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1104 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1106 * @param chargingProfile -
1107 * @param currentDate -
1108 * @param logPrefix -
1110 const prepareRecurringChargingProfile
= (
1111 chargingProfile
: ChargingProfile
,
1112 currentDate
: string | number | Date,
1115 const chargingSchedule
= chargingProfile
.chargingSchedule
1116 let recurringIntervalTranslated
= false
1117 let recurringInterval
: Interval
| undefined
1118 switch (chargingProfile
.recurrencyKind
) {
1119 case RecurrencyKindType
.DAILY
:
1120 recurringInterval
= {
1121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1122 start
: chargingSchedule
.startSchedule
!,
1123 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1124 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1126 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1128 !isWithinInterval(currentDate
, recurringInterval
) &&
1129 isBefore(recurringInterval
.end
, currentDate
)
1131 chargingSchedule
.startSchedule
= addDays(
1132 recurringInterval
.start
,
1133 differenceInDays(currentDate
, recurringInterval
.start
)
1135 recurringInterval
= {
1136 start
: chargingSchedule
.startSchedule
,
1137 end
: addDays(chargingSchedule
.startSchedule
, 1)
1139 recurringIntervalTranslated
= true
1142 case RecurrencyKindType
.WEEKLY
:
1143 recurringInterval
= {
1144 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1145 start
: chargingSchedule
.startSchedule
!,
1146 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1147 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1149 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1151 !isWithinInterval(currentDate
, recurringInterval
) &&
1152 isBefore(recurringInterval
.end
, currentDate
)
1154 chargingSchedule
.startSchedule
= addWeeks(
1155 recurringInterval
.start
,
1156 differenceInWeeks(currentDate
, recurringInterval
.start
)
1158 recurringInterval
= {
1159 start
: chargingSchedule
.startSchedule
,
1160 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1162 recurringIntervalTranslated
= true
1167 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1170 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1171 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1173 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1174 chargingProfile.recurrencyKind
1175 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1176 recurringInterval?.start as Date
1177 ).toISOString()}, ${toDate(
1178 recurringInterval?.end as Date
1179 ).toISOString()}] has not been properly translated to current date ${
1180 isDate(currentDate) ? currentDate.toISOString() : currentDate
1184 return recurringIntervalTranslated
1187 const checkRecurringChargingProfileDuration
= (
1188 chargingProfile
: ChargingProfile
,
1192 if (chargingProfile
.chargingSchedule
.duration
== null) {
1194 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1195 chargingProfile.chargingProfileKind
1196 } charging profile id ${
1197 chargingProfile.chargingProfileId
1198 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1203 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1205 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1208 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1209 chargingProfile.chargingProfileKind
1210 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1211 chargingProfile.chargingSchedule.duration
1212 } is greater than the recurrency time interval duration ${differenceInSeconds(
1217 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1221 const getRandomSerialNumberSuffix
= (params
?: {
1222 randomBytesLength
?: number
1225 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1226 if (params
?.upperCase
=== true) {
1227 return randomSerialNumberSuffix
.toUpperCase()
1229 return randomSerialNumberSuffix