1 import { createHash
, randomBytes
} from
'node:crypto'
2 import type { EventEmitter
} from
'node:events'
3 import { basename
, dirname
, isAbsolute
, join
, parse
, relative
, resolve
} from
'node:path'
4 import { env
} from
'node:process'
5 import { fileURLToPath
} from
'node:url'
7 import chalk from
'chalk'
23 import { maxTime
} from
'date-fns/constants'
24 import { isEmpty
} from
'rambda'
26 import { BaseError
} from
'../exception/index.js'
30 type BootNotificationRequest
,
33 ChargingProfileKindType
,
34 ChargingProfilePurposeType
,
36 type ChargingSchedulePeriod
,
37 type ChargingStationConfiguration
,
38 type ChargingStationInfo
,
39 type ChargingStationOptions
,
40 type ChargingStationTemplate
,
41 type ChargingStationWorkerMessageEvents
,
42 ConnectorPhaseRotation
,
47 type OCPP16BootNotificationRequest
,
48 type OCPP20BootNotificationRequest
,
52 ReservationTerminationReason
,
53 StandardParametersKey
,
54 type SupportedFeatureProfiles
,
56 } from
'../types/index.js'
70 } from
'../utils/index.js'
71 import type { ChargingStation
} from
'./ChargingStation.js'
72 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
74 const moduleName
= 'Helpers'
76 export const buildTemplateName
= (templateFile
: string): string => {
77 if (isAbsolute(templateFile
)) {
78 templateFile
= relative(
79 resolve(join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'station-templates')),
83 const templateFileParsedPath
= parse(templateFile
)
84 return join(templateFileParsedPath
.dir
, templateFileParsedPath
.name
)
87 export const getChargingStationId
= (
89 stationTemplate
: ChargingStationTemplate
| undefined
91 if (stationTemplate
== null) {
92 return "Unknown 'chargingStationId'"
94 // In case of multiple instances: add instance index to charging station id
95 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
96 const idSuffix
= stationTemplate
.nameSuffix
?? ''
97 const idStr
= `000000000${index.toString()}`
98 return stationTemplate
.fixedName
=== true
99 ? stationTemplate
.baseName
100 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
105 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
106 return isPast(reservation
.expiryDate
)
109 export const removeExpiredReservations
= async (
110 chargingStation
: ChargingStation
111 ): Promise
<void> => {
112 if (chargingStation
.hasEvses
) {
113 for (const evseStatus
of chargingStation
.evses
.values()) {
114 for (const connectorStatus
of evseStatus
.connectors
.values()) {
116 connectorStatus
.reservation
!= null &&
117 hasReservationExpired(connectorStatus
.reservation
)
119 await chargingStation
.removeReservation(
120 connectorStatus
.reservation
,
121 ReservationTerminationReason
.EXPIRED
127 for (const connectorStatus
of chargingStation
.connectors
.values()) {
129 connectorStatus
.reservation
!= null &&
130 hasReservationExpired(connectorStatus
.reservation
)
132 await chargingStation
.removeReservation(
133 connectorStatus
.reservation
,
134 ReservationTerminationReason
.EXPIRED
141 export const getNumberOfReservableConnectors
= (
142 connectors
: Map
<number, ConnectorStatus
>
144 let numberOfReservableConnectors
= 0
145 for (const [connectorId
, connectorStatus
] of connectors
) {
146 if (connectorId
=== 0) {
149 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
150 ++numberOfReservableConnectors
153 return numberOfReservableConnectors
156 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
157 const chargingStationInfo
= {
158 chargePointModel
: stationTemplate
.chargePointModel
,
159 chargePointVendor
: stationTemplate
.chargePointVendor
,
160 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!= null && {
161 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
163 ...(stationTemplate
.chargePointSerialNumberPrefix
!= null && {
164 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
166 ...(stationTemplate
.meterSerialNumberPrefix
!= null && {
167 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
169 ...(stationTemplate
.meterType
!= null && {
170 meterType
: stationTemplate
.meterType
173 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
174 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
178 export const checkChargingStation
= (
179 chargingStation
: ChargingStation
,
182 if (!chargingStation
.started
&& !chargingStation
.starting
) {
183 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
189 export const getPhaseRotationValue
= (
191 numberOfPhases
: number
192 ): string | undefined => {
194 if (connectorId
=== 0 && numberOfPhases
=== 0) {
195 return `${connectorId}.${ConnectorPhaseRotation.RST}`
196 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
197 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
199 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
200 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
201 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
202 return `${connectorId}.${ConnectorPhaseRotation.RST}`
206 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
210 return Object.keys(evses
).length
213 const getMaxNumberOfConnectors
= (
214 connectors
: Record
<string, ConnectorStatus
> | undefined
216 if (connectors
== null) {
219 return Object.keys(connectors
).length
222 export const getBootConnectorStatus
= (
223 chargingStation
: ChargingStation
,
225 connectorStatus
: ConnectorStatus
226 ): ConnectorStatusEnum
=> {
227 let connectorBootStatus
: ConnectorStatusEnum
229 connectorStatus
.status == null &&
230 (!chargingStation
.isChargingStationAvailable() ||
231 !chargingStation
.isConnectorAvailable(connectorId
))
233 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
234 } else if (connectorStatus
.status == null && connectorStatus
.bootStatus
!= null) {
235 // Set boot status in template at startup
236 connectorBootStatus
= connectorStatus
.bootStatus
237 } else if (connectorStatus
.status != null) {
238 // Set previous status at startup
239 connectorBootStatus
= connectorStatus
.status
241 // Set default status
242 connectorBootStatus
= ConnectorStatusEnum
.Available
244 return connectorBootStatus
247 export const checkTemplate
= (
248 stationTemplate
: ChargingStationTemplate
| undefined,
252 if (stationTemplate
== null) {
253 const errorMsg
= `Failed to read charging station template file ${templateFile}`
254 logger
.error(`${logPrefix} ${errorMsg}`)
255 throw new BaseError(errorMsg
)
257 if (isEmpty(stationTemplate
)) {
258 const errorMsg
= `Empty charging station information from template file ${templateFile}`
259 logger
.error(`${logPrefix} ${errorMsg}`)
260 throw new BaseError(errorMsg
)
262 if (stationTemplate
.idTagsFile
== null || isEmpty(stationTemplate
.idTagsFile
)) {
264 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
269 export const checkConfiguration
= (
270 stationConfiguration
: ChargingStationConfiguration
| undefined,
272 configurationFile
: string
274 if (stationConfiguration
== null) {
275 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
276 logger
.error(`${logPrefix} ${errorMsg}`)
277 throw new BaseError(errorMsg
)
279 if (isEmpty(stationConfiguration
)) {
280 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
281 logger
.error(`${logPrefix} ${errorMsg}`)
282 throw new BaseError(errorMsg
)
286 export const checkConnectorsConfiguration
= (
287 stationTemplate
: ChargingStationTemplate
,
291 configuredMaxConnectors
: number
292 templateMaxConnectors
: number
293 templateMaxAvailableConnectors
: number
295 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
296 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
297 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
298 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
299 const templateMaxAvailableConnectors
=
300 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
302 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
303 stationTemplate
.randomConnectors
!== true
306 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
308 stationTemplate
.randomConnectors
= true
311 configuredMaxConnectors
,
312 templateMaxConnectors
,
313 templateMaxAvailableConnectors
317 export const checkStationInfoConnectorStatus
= (
319 connectorStatus
: ConnectorStatus
,
323 if (connectorStatus
.status != null) {
325 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
327 delete connectorStatus
.status
331 export const setChargingStationOptions
= (
332 stationInfo
: ChargingStationInfo
,
333 options
?: ChargingStationOptions
334 ): ChargingStationInfo
=> {
335 if (options
?.supervisionUrls
!= null) {
336 stationInfo
.supervisionUrls
= options
.supervisionUrls
338 if (options
?.persistentConfiguration
!= null) {
339 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
340 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
341 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
342 options
.persistentConfiguration
344 if (options
?.autoStart
!= null) {
345 stationInfo
.autoStart
= options
.autoStart
347 if (options
?.autoRegister
!= null) {
348 stationInfo
.autoRegister
= options
.autoRegister
350 if (options
?.enableStatistics
!= null) {
351 stationInfo
.enableStatistics
= options
.enableStatistics
353 if (options
?.ocppStrictCompliance
!= null) {
354 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
356 if (options
?.stopTransactionsOnStopped
!= null) {
357 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
362 export const buildConnectorsMap
= (
363 connectors
: Record
<string, ConnectorStatus
>,
366 ): Map
<number, ConnectorStatus
> => {
367 const connectorsMap
= new Map
<number, ConnectorStatus
>()
368 if (getMaxNumberOfConnectors(connectors
) > 0) {
369 for (const connector
in connectors
) {
370 const connectorStatus
= connectors
[connector
]
371 const connectorId
= convertToInt(connector
)
372 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
373 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
377 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
383 export const initializeConnectorsMapStatus
= (
384 connectors
: Map
<number, ConnectorStatus
>,
387 for (const connectorId
of connectors
.keys()) {
388 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
390 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
391 connectors.get(connectorId)?.transactionId
395 if (connectorId
=== 0) {
396 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
397 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
398 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 connectors
.get(connectorId
)!.chargingProfiles
= []
402 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
403 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
404 initializeConnectorStatus(connectors
.get(connectorId
)!)
409 export const resetAuthorizeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
410 connectorStatus
.idTagLocalAuthorized
= false
411 connectorStatus
.idTagAuthorized
= false
412 delete connectorStatus
.localAuthorizeIdTag
413 delete connectorStatus
.authorizeIdTag
416 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
417 if (connectorStatus
== null) {
420 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
421 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
.filter(
423 chargingProfile
.chargingProfilePurpose
!== ChargingProfilePurposeType
.TX_PROFILE
||
424 (chargingProfile
.transactionId
!= null &&
425 connectorStatus
.transactionId
!= null &&
426 chargingProfile
.transactionId
!== connectorStatus
.transactionId
)
429 resetAuthorizeConnectorStatus(connectorStatus
)
430 connectorStatus
.transactionRemoteStarted
= false
431 connectorStatus
.transactionStarted
= false
432 delete connectorStatus
.transactionStart
433 delete connectorStatus
.transactionId
434 delete connectorStatus
.transactionIdTag
435 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
436 delete connectorStatus
.transactionBeginMeterValue
439 export const prepareDatesInConnectorStatus
= (
440 connectorStatus
: ConnectorStatus
441 ): ConnectorStatus
=> {
442 if (connectorStatus
.reservation
!= null) {
443 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
444 connectorStatus
.reservation
.expiryDate
= convertToDate(connectorStatus
.reservation
.expiryDate
)!
446 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
447 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
.map(chargingProfile
=> {
448 chargingProfile
.chargingSchedule
.startSchedule
= convertToDate(
449 chargingProfile
.chargingSchedule
.startSchedule
451 chargingProfile
.validFrom
= convertToDate(chargingProfile
.validFrom
)
452 chargingProfile
.validTo
= convertToDate(chargingProfile
.validTo
)
453 return chargingProfile
456 return connectorStatus
459 export const createBootNotificationRequest
= (
460 stationInfo
: ChargingStationInfo
,
461 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
462 ): BootNotificationRequest
| undefined => {
463 const ocppVersion
= stationInfo
.ocppVersion
464 switch (ocppVersion
) {
465 case OCPPVersion
.VERSION_16
:
467 chargePointModel
: stationInfo
.chargePointModel
,
468 chargePointVendor
: stationInfo
.chargePointVendor
,
469 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
470 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
472 ...(stationInfo
.chargePointSerialNumber
!= null && {
473 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
475 ...(stationInfo
.firmwareVersion
!= null && {
476 firmwareVersion
: stationInfo
.firmwareVersion
478 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
479 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
480 ...(stationInfo
.meterSerialNumber
!= null && {
481 meterSerialNumber
: stationInfo
.meterSerialNumber
483 ...(stationInfo
.meterType
!= null && {
484 meterType
: stationInfo
.meterType
486 } satisfies OCPP16BootNotificationRequest
487 case OCPPVersion
.VERSION_20
:
488 case OCPPVersion
.VERSION_201
:
492 model
: stationInfo
.chargePointModel
,
493 vendorName
: stationInfo
.chargePointVendor
,
494 ...(stationInfo
.firmwareVersion
!= null && {
495 firmwareVersion
: stationInfo
.firmwareVersion
497 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
498 serialNumber
: stationInfo
.chargeBoxSerialNumber
500 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
502 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
503 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
507 } satisfies OCPP20BootNotificationRequest
511 export const warnTemplateKeysDeprecation
= (
512 stationTemplate
: ChargingStationTemplate
,
516 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
517 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
518 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
519 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
520 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
522 for (const templateKey
of templateKeys
) {
523 warnDeprecatedTemplateKey(
525 templateKey
.deprecatedKey
,
528 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
530 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
534 export const stationTemplateToStationInfo
= (
535 stationTemplate
: ChargingStationTemplate
536 ): ChargingStationInfo
=> {
537 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
538 delete stationTemplate
.power
539 delete stationTemplate
.powerUnit
540 delete stationTemplate
.Connectors
541 delete stationTemplate
.Evses
542 delete stationTemplate
.Configuration
543 delete stationTemplate
.AutomaticTransactionGenerator
544 delete stationTemplate
.numberOfConnectors
545 delete stationTemplate
.chargeBoxSerialNumberPrefix
546 delete stationTemplate
.chargePointSerialNumberPrefix
547 delete stationTemplate
.meterSerialNumberPrefix
548 return stationTemplate
as ChargingStationInfo
551 export const createSerialNumber
= (
552 stationTemplate
: ChargingStationTemplate
,
553 stationInfo
: ChargingStationInfo
,
555 randomSerialNumberUpperCase
?: boolean
556 randomSerialNumber
?: boolean
560 ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true },
563 const serialNumberSuffix
=
564 params
.randomSerialNumber
=== true
565 ? getRandomSerialNumberSuffix({
566 upperCase
: params
.randomSerialNumberUpperCase
569 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
570 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
571 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
572 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
573 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
574 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
577 export const propagateSerialNumber
= (
578 stationTemplate
: ChargingStationTemplate
| undefined,
579 stationInfoSrc
: ChargingStationInfo
| undefined,
580 stationInfoDst
: ChargingStationInfo
582 if (stationInfoSrc
== null || stationTemplate
== null) {
584 'Missing charging station template or existing configuration to propagate serial number'
587 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
588 stationInfoSrc
.chargePointSerialNumber
!= null
589 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
590 : stationInfoDst
.chargePointSerialNumber
!= null &&
591 delete stationInfoDst
.chargePointSerialNumber
592 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
593 stationInfoSrc
.chargeBoxSerialNumber
!= null
594 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
595 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
596 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
597 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
598 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
601 export const hasFeatureProfile
= (
602 chargingStation
: ChargingStation
,
603 featureProfile
: SupportedFeatureProfiles
604 ): boolean | undefined => {
605 return getConfigurationKey(
607 StandardParametersKey
.SupportedFeatureProfiles
608 )?.value
?.includes(featureProfile
)
611 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
613 switch (stationInfo
.amperageLimitationUnit
) {
614 case AmpereUnits
.DECI_AMPERE
:
617 case AmpereUnits
.CENTI_AMPERE
:
620 case AmpereUnits
.MILLI_AMPERE
:
628 * Gets the connector cloned charging profiles applying a power limitation
629 * and sorted by connector id descending then stack level descending
631 * @param chargingStation -
632 * @param connectorId -
633 * @returns connector charging profiles array
635 export const getConnectorChargingProfiles
= (
636 chargingStation
: ChargingStation
,
638 ): ChargingProfile
[] => {
639 return clone
<ChargingProfile
[]>(
640 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
641 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
643 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
644 (a
, b
) => b
.stackLevel
- a
.stackLevel
650 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
651 chargingStation
: ChargingStation
,
653 ): number | undefined => {
654 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
655 // Get charging profiles sorted by connector id then stack level
656 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
657 if (isNotEmptyArray(chargingProfiles
)) {
658 const result
= getLimitFromChargingProfiles(
662 chargingStation
.logPrefix()
664 if (result
!= null) {
666 chargingProfile
= result
.chargingProfile
667 switch (chargingStation
.stationInfo
?.currentOutType
) {
670 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
672 : ACElectricUtils
.powerTotal(
673 chargingStation
.getNumberOfPhases(),
674 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
675 chargingStation
.stationInfo
.voltageOut
!,
681 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
683 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
684 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
686 const connectorMaximumPower
=
687 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
688 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
689 if (limit
> connectorMaximumPower
) {
691 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
692 chargingProfile.chargingProfileId
693 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
696 limit
= connectorMaximumPower
703 export const getDefaultVoltageOut
= (
704 currentType
: CurrentType
,
708 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
709 let defaultVoltageOut
: number
710 switch (currentType
) {
712 defaultVoltageOut
= Voltage
.VOLTAGE_230
715 defaultVoltageOut
= Voltage
.VOLTAGE_400
718 logger
.error(`${logPrefix} ${errorMsg}`)
719 throw new BaseError(errorMsg
)
721 return defaultVoltageOut
724 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
725 return stationInfo
.idTagsFile
!= null
726 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
730 export const waitChargingStationEvents
= async (
731 emitter
: EventEmitter
,
732 event
: ChargingStationWorkerMessageEvents
,
734 ): Promise
<number> => {
735 return await new Promise
<number>(resolve
=> {
737 if (eventsToWait
=== 0) {
741 emitter
.on(event
, () => {
743 if (events
=== eventsToWait
) {
750 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
751 let configuredMaxNumberOfConnectors
= 0
752 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
753 const numberOfConnectors
= stationTemplate
.numberOfConnectors
754 configuredMaxNumberOfConnectors
=
755 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
756 } else if (stationTemplate
.numberOfConnectors
!= null) {
757 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
758 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
759 configuredMaxNumberOfConnectors
=
760 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
761 stationTemplate
.Connectors
[0] != null
762 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
763 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
764 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
765 for (const evse
in stationTemplate
.Evses
) {
769 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
770 stationTemplate
.Evses
[evse
].Connectors
774 return configuredMaxNumberOfConnectors
777 const checkConfiguredMaxConnectors
= (
778 configuredMaxConnectors
: number,
782 if (configuredMaxConnectors
<= 0) {
784 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
789 const checkTemplateMaxConnectors
= (
790 templateMaxConnectors
: number,
794 if (templateMaxConnectors
=== 0) {
796 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
798 } else if (templateMaxConnectors
< 0) {
800 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
805 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
806 connectorStatus
.availability
= AvailabilityType
.Operative
807 connectorStatus
.idTagLocalAuthorized
= false
808 connectorStatus
.idTagAuthorized
= false
809 connectorStatus
.transactionRemoteStarted
= false
810 connectorStatus
.transactionStarted
= false
811 connectorStatus
.energyActiveImportRegisterValue
= 0
812 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
813 if (connectorStatus
.chargingProfiles
== null) {
814 connectorStatus
.chargingProfiles
= []
818 const warnDeprecatedTemplateKey
= (
819 template
: ChargingStationTemplate
,
822 templateFile
: string,
825 if (template
[key
as keyof ChargingStationTemplate
] != null) {
826 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
827 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
829 logger
.warn(`${logPrefix} ${logMsg}`)
830 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
834 const convertDeprecatedTemplateKey
= (
835 template
: ChargingStationTemplate
,
836 deprecatedKey
: string,
839 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
841 (template
as unknown
as Record
<string, unknown
>)[key
] =
842 template
[deprecatedKey
as keyof ChargingStationTemplate
]
844 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
845 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
849 interface ChargingProfilesLimit
{
851 chargingProfile
: ChargingProfile
855 * Charging profiles shall already be sorted by connector id descending then stack level descending
857 * @param chargingStation -
858 * @param connectorId -
859 * @param chargingProfiles -
861 * @returns ChargingProfilesLimit
863 const getLimitFromChargingProfiles
= (
864 chargingStation
: ChargingStation
,
866 chargingProfiles
: ChargingProfile
[],
868 ): ChargingProfilesLimit
| undefined => {
869 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
870 const currentDate
= new Date()
871 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
872 for (const chargingProfile
of chargingProfiles
) {
873 const chargingSchedule
= chargingProfile
.chargingSchedule
874 if (chargingSchedule
.startSchedule
== null) {
876 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
878 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
879 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
881 if (!isDate(chargingSchedule
.startSchedule
)) {
883 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
885 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
886 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
888 if (chargingSchedule
.duration
== null) {
890 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
892 // OCPP specifies that if duration is not defined, it should be infinite
893 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
895 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
898 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
901 // Check if the charging profile is active
903 isWithinInterval(currentDate
, {
904 start
: chargingSchedule
.startSchedule
,
905 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
908 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
909 const chargingSchedulePeriodCompareFn
= (
910 a
: ChargingSchedulePeriod
,
911 b
: ChargingSchedulePeriod
912 ): number => a
.startPeriod
- b
.startPeriod
914 !isArraySorted
<ChargingSchedulePeriod
>(
915 chargingSchedule
.chargingSchedulePeriod
,
916 chargingSchedulePeriodCompareFn
920 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
922 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
924 // Check if the first schedule period startPeriod property is equal to 0
925 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
927 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
931 // Handle only one schedule period
932 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
933 const result
: ChargingProfilesLimit
= {
934 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
937 logger
.debug(debugLogMsg
, result
)
940 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
941 // Search for the right schedule period
944 chargingSchedulePeriod
945 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
946 // Find the right schedule period
949 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
953 // Found the schedule period: previous is the correct one
954 const result
: ChargingProfilesLimit
= {
955 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
956 limit
: previousChargingSchedulePeriod
!.limit
,
959 logger
.debug(debugLogMsg
, result
)
962 // Keep a reference to previous one
963 previousChargingSchedulePeriod
= chargingSchedulePeriod
964 // Handle the last schedule period within the charging profile duration
966 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
967 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
970 chargingSchedule
.startSchedule
,
971 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
973 chargingSchedule
.startSchedule
974 ) > chargingSchedule
.duration
)
976 const result
: ChargingProfilesLimit
= {
977 limit
: previousChargingSchedulePeriod
.limit
,
980 logger
.debug(debugLogMsg
, result
)
989 export const prepareChargingProfileKind
= (
990 connectorStatus
: ConnectorStatus
| undefined,
991 chargingProfile
: ChargingProfile
,
992 currentDate
: string | number | Date,
995 switch (chargingProfile
.chargingProfileKind
) {
996 case ChargingProfileKindType
.RECURRING
:
997 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
1000 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
1002 case ChargingProfileKindType
.RELATIVE
:
1003 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
1005 `${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`
1007 delete chargingProfile
.chargingSchedule
.startSchedule
1009 if (connectorStatus
?.transactionStarted
=== true) {
1010 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
1012 // FIXME: handle relative charging profile duration
1018 export const canProceedChargingProfile
= (
1019 chargingProfile
: ChargingProfile
,
1020 currentDate
: string | number | Date,
1024 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1025 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1028 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1029 chargingProfile.chargingProfileId
1030 } is not valid for the current date ${
1031 isDate(currentDate) ? currentDate.toISOString() : currentDate
1037 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1038 chargingProfile
.chargingSchedule
.duration
== null
1041 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1045 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1047 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1051 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1053 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1060 const canProceedRecurringChargingProfile
= (
1061 chargingProfile
: ChargingProfile
,
1065 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1066 chargingProfile
.recurrencyKind
== null
1069 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1074 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1075 chargingProfile
.chargingSchedule
.startSchedule
== null
1078 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1086 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1088 * @param chargingProfile -
1089 * @param currentDate -
1090 * @param logPrefix -
1092 const prepareRecurringChargingProfile
= (
1093 chargingProfile
: ChargingProfile
,
1094 currentDate
: string | number | Date,
1097 const chargingSchedule
= chargingProfile
.chargingSchedule
1098 let recurringIntervalTranslated
= false
1099 let recurringInterval
: Interval
| undefined
1100 switch (chargingProfile
.recurrencyKind
) {
1101 case RecurrencyKindType
.DAILY
:
1102 recurringInterval
= {
1103 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1104 start
: chargingSchedule
.startSchedule
!,
1105 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1106 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1108 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1110 !isWithinInterval(currentDate
, recurringInterval
) &&
1111 isBefore(recurringInterval
.end
, currentDate
)
1113 chargingSchedule
.startSchedule
= addDays(
1114 recurringInterval
.start
,
1115 differenceInDays(currentDate
, recurringInterval
.start
)
1117 recurringInterval
= {
1118 start
: chargingSchedule
.startSchedule
,
1119 end
: addDays(chargingSchedule
.startSchedule
, 1)
1121 recurringIntervalTranslated
= true
1124 case RecurrencyKindType
.WEEKLY
:
1125 recurringInterval
= {
1126 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1127 start
: chargingSchedule
.startSchedule
!,
1128 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1129 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1131 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1133 !isWithinInterval(currentDate
, recurringInterval
) &&
1134 isBefore(recurringInterval
.end
, currentDate
)
1136 chargingSchedule
.startSchedule
= addWeeks(
1137 recurringInterval
.start
,
1138 differenceInWeeks(currentDate
, recurringInterval
.start
)
1140 recurringInterval
= {
1141 start
: chargingSchedule
.startSchedule
,
1142 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1144 recurringIntervalTranslated
= true
1149 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1152 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1153 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1155 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1156 chargingProfile.recurrencyKind
1157 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1158 recurringInterval?.start as Date
1159 ).toISOString()}, ${toDate(
1160 recurringInterval?.end as Date
1161 ).toISOString()}] has not been properly translated to current date ${
1162 isDate(currentDate) ? currentDate.toISOString() : currentDate
1166 return recurringIntervalTranslated
1169 const checkRecurringChargingProfileDuration
= (
1170 chargingProfile
: ChargingProfile
,
1174 if (chargingProfile
.chargingSchedule
.duration
== null) {
1176 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1177 chargingProfile.chargingProfileKind
1178 } charging profile id ${
1179 chargingProfile.chargingProfileId
1180 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1185 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1187 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1190 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1191 chargingProfile.chargingProfileKind
1192 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1193 chargingProfile.chargingSchedule.duration
1194 } is greater than the recurrency time interval duration ${differenceInSeconds(
1199 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1203 const getRandomSerialNumberSuffix
= (params
?: {
1204 randomBytesLength
?: number
1207 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1208 if (params
?.upperCase
=== true) {
1209 return randomSerialNumberSuffix
.toUpperCase()
1211 return randomSerialNumberSuffix