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
,
35 type ChargingSchedulePeriod
,
36 type ChargingStationConfiguration
,
37 type ChargingStationInfo
,
38 type ChargingStationOptions
,
39 type ChargingStationTemplate
,
40 type ChargingStationWorkerMessageEvents
,
41 ConnectorPhaseRotation
,
46 type OCPP16BootNotificationRequest
,
47 type OCPP20BootNotificationRequest
,
51 ReservationTerminationReason
,
52 StandardParametersKey
,
53 type SupportedFeatureProfiles
,
55 } from
'../types/index.js'
69 } from
'../utils/index.js'
70 import type { ChargingStation
} from
'./ChargingStation.js'
71 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
73 const moduleName
= 'Helpers'
75 export const buildTemplateName
= (templateFile
: string): string => {
76 if (isAbsolute(templateFile
)) {
77 templateFile
= relative(
78 resolve(join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'station-templates')),
82 const templateFileParsedPath
= parse(templateFile
)
83 return join(templateFileParsedPath
.dir
, templateFileParsedPath
.name
)
86 export const getChargingStationId
= (
88 stationTemplate
: ChargingStationTemplate
| undefined
90 if (stationTemplate
== null) {
91 return "Unknown 'chargingStationId'"
93 // In case of multiple instances: add instance index to charging station id
94 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
95 const idSuffix
= stationTemplate
.nameSuffix
?? ''
96 const idStr
= `000000000${index.toString()}`
97 return stationTemplate
.fixedName
=== true
98 ? stationTemplate
.baseName
99 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
104 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
105 return isPast(reservation
.expiryDate
)
108 export const removeExpiredReservations
= async (
109 chargingStation
: ChargingStation
110 ): Promise
<void> => {
111 if (chargingStation
.hasEvses
) {
112 for (const evseStatus
of chargingStation
.evses
.values()) {
113 for (const connectorStatus
of evseStatus
.connectors
.values()) {
115 connectorStatus
.reservation
!= null &&
116 hasReservationExpired(connectorStatus
.reservation
)
118 await chargingStation
.removeReservation(
119 connectorStatus
.reservation
,
120 ReservationTerminationReason
.EXPIRED
126 for (const connectorStatus
of chargingStation
.connectors
.values()) {
128 connectorStatus
.reservation
!= null &&
129 hasReservationExpired(connectorStatus
.reservation
)
131 await chargingStation
.removeReservation(
132 connectorStatus
.reservation
,
133 ReservationTerminationReason
.EXPIRED
140 export const getNumberOfReservableConnectors
= (
141 connectors
: Map
<number, ConnectorStatus
>
143 let numberOfReservableConnectors
= 0
144 for (const [connectorId
, connectorStatus
] of connectors
) {
145 if (connectorId
=== 0) {
148 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
149 ++numberOfReservableConnectors
152 return numberOfReservableConnectors
155 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
156 const chargingStationInfo
= {
157 chargePointModel
: stationTemplate
.chargePointModel
,
158 chargePointVendor
: stationTemplate
.chargePointVendor
,
159 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!= null && {
160 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
162 ...(stationTemplate
.chargePointSerialNumberPrefix
!= null && {
163 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
165 ...(stationTemplate
.meterSerialNumberPrefix
!= null && {
166 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
168 ...(stationTemplate
.meterType
!= null && {
169 meterType
: stationTemplate
.meterType
172 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
173 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
177 export const checkChargingStation
= (
178 chargingStation
: ChargingStation
,
181 if (!chargingStation
.started
&& !chargingStation
.starting
) {
182 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
188 export const getPhaseRotationValue
= (
190 numberOfPhases
: number
191 ): string | undefined => {
193 if (connectorId
=== 0 && numberOfPhases
=== 0) {
194 return `${connectorId}.${ConnectorPhaseRotation.RST}`
195 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
196 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
198 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
199 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
200 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
201 return `${connectorId}.${ConnectorPhaseRotation.RST}`
205 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
209 return Object.keys(evses
).length
212 const getMaxNumberOfConnectors
= (
213 connectors
: Record
<string, ConnectorStatus
> | undefined
215 if (connectors
== null) {
218 return Object.keys(connectors
).length
221 export const getBootConnectorStatus
= (
222 chargingStation
: ChargingStation
,
224 connectorStatus
: ConnectorStatus
225 ): ConnectorStatusEnum
=> {
226 let connectorBootStatus
: ConnectorStatusEnum
228 connectorStatus
.status == null &&
229 (!chargingStation
.isChargingStationAvailable() ||
230 !chargingStation
.isConnectorAvailable(connectorId
))
232 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
233 } else if (connectorStatus
.status == null && connectorStatus
.bootStatus
!= null) {
234 // Set boot status in template at startup
235 connectorBootStatus
= connectorStatus
.bootStatus
236 } else if (connectorStatus
.status != null) {
237 // Set previous status at startup
238 connectorBootStatus
= connectorStatus
.status
240 // Set default status
241 connectorBootStatus
= ConnectorStatusEnum
.Available
243 return connectorBootStatus
246 export const checkTemplate
= (
247 stationTemplate
: ChargingStationTemplate
| undefined,
251 if (stationTemplate
== null) {
252 const errorMsg
= `Failed to read charging station template file ${templateFile}`
253 logger
.error(`${logPrefix} ${errorMsg}`)
254 throw new BaseError(errorMsg
)
256 if (isEmpty(stationTemplate
)) {
257 const errorMsg
= `Empty charging station information from template file ${templateFile}`
258 logger
.error(`${logPrefix} ${errorMsg}`)
259 throw new BaseError(errorMsg
)
261 if (stationTemplate
.idTagsFile
== null || isEmpty(stationTemplate
.idTagsFile
)) {
263 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
268 export const checkConfiguration
= (
269 stationConfiguration
: ChargingStationConfiguration
| undefined,
271 configurationFile
: string
273 if (stationConfiguration
== null) {
274 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
275 logger
.error(`${logPrefix} ${errorMsg}`)
276 throw new BaseError(errorMsg
)
278 if (isEmpty(stationConfiguration
)) {
279 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
280 logger
.error(`${logPrefix} ${errorMsg}`)
281 throw new BaseError(errorMsg
)
285 export const checkConnectorsConfiguration
= (
286 stationTemplate
: ChargingStationTemplate
,
290 configuredMaxConnectors
: number
291 templateMaxConnectors
: number
292 templateMaxAvailableConnectors
: number
294 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
295 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
296 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
297 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
298 const templateMaxAvailableConnectors
=
299 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
301 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
302 stationTemplate
.randomConnectors
!== true
305 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
307 stationTemplate
.randomConnectors
= true
310 configuredMaxConnectors
,
311 templateMaxConnectors
,
312 templateMaxAvailableConnectors
316 export const checkStationInfoConnectorStatus
= (
318 connectorStatus
: ConnectorStatus
,
322 if (connectorStatus
.status != null) {
324 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
326 delete connectorStatus
.status
330 export const setChargingStationOptions
= (
331 stationInfo
: ChargingStationInfo
,
332 options
?: ChargingStationOptions
333 ): ChargingStationInfo
=> {
334 if (options
?.supervisionUrls
!= null) {
335 stationInfo
.supervisionUrls
= options
.supervisionUrls
337 if (options
?.persistentConfiguration
!= null) {
338 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
339 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
340 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
341 options
.persistentConfiguration
343 if (options
?.autoStart
!= null) {
344 stationInfo
.autoStart
= options
.autoStart
346 if (options
?.autoRegister
!= null) {
347 stationInfo
.autoRegister
= options
.autoRegister
349 if (options
?.enableStatistics
!= null) {
350 stationInfo
.enableStatistics
= options
.enableStatistics
352 if (options
?.ocppStrictCompliance
!= null) {
353 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
355 if (options
?.stopTransactionsOnStopped
!= null) {
356 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
361 export const buildConnectorsMap
= (
362 connectors
: Record
<string, ConnectorStatus
>,
365 ): Map
<number, ConnectorStatus
> => {
366 const connectorsMap
= new Map
<number, ConnectorStatus
>()
367 if (getMaxNumberOfConnectors(connectors
) > 0) {
368 for (const connector
in connectors
) {
369 const connectorStatus
= connectors
[connector
]
370 const connectorId
= convertToInt(connector
)
371 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
372 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
376 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
382 export const initializeConnectorsMapStatus
= (
383 connectors
: Map
<number, ConnectorStatus
>,
386 for (const connectorId
of connectors
.keys()) {
387 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
389 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
390 connectors.get(connectorId)?.transactionId
394 if (connectorId
=== 0) {
395 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
396 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
397 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
398 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
399 connectors
.get(connectorId
)!.chargingProfiles
= []
401 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
402 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
403 initializeConnectorStatus(connectors
.get(connectorId
)!)
408 export const resetAuthorizeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
409 connectorStatus
.idTagLocalAuthorized
= false
410 connectorStatus
.idTagAuthorized
= false
411 delete connectorStatus
.localAuthorizeIdTag
412 delete connectorStatus
.authorizeIdTag
415 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
416 if (connectorStatus
== null) {
419 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
420 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
.filter(
422 (chargingProfile
.transactionId
!= null &&
423 connectorStatus
.transactionId
!= null &&
424 chargingProfile
.transactionId
!== connectorStatus
.transactionId
) ||
425 chargingProfile
.transactionId
== null
428 resetAuthorizeConnectorStatus(connectorStatus
)
429 connectorStatus
.transactionRemoteStarted
= false
430 connectorStatus
.transactionStarted
= false
431 delete connectorStatus
.transactionStart
432 delete connectorStatus
.transactionId
433 delete connectorStatus
.transactionIdTag
434 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
435 delete connectorStatus
.transactionBeginMeterValue
438 export const prepareDatesInConnectorStatus
= (
439 connectorStatus
: ConnectorStatus
440 ): 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
.map(chargingProfile
=> {
447 chargingProfile
.chargingSchedule
.startSchedule
= convertToDate(
448 chargingProfile
.chargingSchedule
.startSchedule
450 chargingProfile
.validFrom
= convertToDate(chargingProfile
.validFrom
)
451 chargingProfile
.validTo
= convertToDate(chargingProfile
.validTo
)
452 return chargingProfile
455 return connectorStatus
458 export const createBootNotificationRequest
= (
459 stationInfo
: ChargingStationInfo
,
460 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
461 ): BootNotificationRequest
| undefined => {
462 const ocppVersion
= stationInfo
.ocppVersion
463 switch (ocppVersion
) {
464 case OCPPVersion
.VERSION_16
:
466 chargePointModel
: stationInfo
.chargePointModel
,
467 chargePointVendor
: stationInfo
.chargePointVendor
,
468 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
469 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
471 ...(stationInfo
.chargePointSerialNumber
!= null && {
472 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
474 ...(stationInfo
.firmwareVersion
!= null && {
475 firmwareVersion
: stationInfo
.firmwareVersion
477 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
478 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
479 ...(stationInfo
.meterSerialNumber
!= null && {
480 meterSerialNumber
: stationInfo
.meterSerialNumber
482 ...(stationInfo
.meterType
!= null && {
483 meterType
: stationInfo
.meterType
485 } satisfies OCPP16BootNotificationRequest
486 case OCPPVersion
.VERSION_20
:
487 case OCPPVersion
.VERSION_201
:
491 model
: stationInfo
.chargePointModel
,
492 vendorName
: stationInfo
.chargePointVendor
,
493 ...(stationInfo
.firmwareVersion
!= null && {
494 firmwareVersion
: stationInfo
.firmwareVersion
496 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
497 serialNumber
: stationInfo
.chargeBoxSerialNumber
499 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
501 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
502 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
506 } satisfies OCPP20BootNotificationRequest
510 export const warnTemplateKeysDeprecation
= (
511 stationTemplate
: ChargingStationTemplate
,
515 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
516 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
517 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
518 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
519 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
521 for (const templateKey
of templateKeys
) {
522 warnDeprecatedTemplateKey(
524 templateKey
.deprecatedKey
,
527 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
529 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
533 export const stationTemplateToStationInfo
= (
534 stationTemplate
: ChargingStationTemplate
535 ): ChargingStationInfo
=> {
536 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
537 delete stationTemplate
.power
538 delete stationTemplate
.powerUnit
539 delete stationTemplate
.Connectors
540 delete stationTemplate
.Evses
541 delete stationTemplate
.Configuration
542 delete stationTemplate
.AutomaticTransactionGenerator
543 delete stationTemplate
.numberOfConnectors
544 delete stationTemplate
.chargeBoxSerialNumberPrefix
545 delete stationTemplate
.chargePointSerialNumberPrefix
546 delete stationTemplate
.meterSerialNumberPrefix
547 return stationTemplate
as ChargingStationInfo
550 export const createSerialNumber
= (
551 stationTemplate
: ChargingStationTemplate
,
552 stationInfo
: ChargingStationInfo
,
554 randomSerialNumberUpperCase
?: boolean
555 randomSerialNumber
?: boolean
559 ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true },
562 const serialNumberSuffix
=
563 params
.randomSerialNumber
=== true
564 ? getRandomSerialNumberSuffix({
565 upperCase
: params
.randomSerialNumberUpperCase
568 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
569 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
570 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
571 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
572 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
573 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
576 export const propagateSerialNumber
= (
577 stationTemplate
: ChargingStationTemplate
| undefined,
578 stationInfoSrc
: ChargingStationInfo
| undefined,
579 stationInfoDst
: ChargingStationInfo
581 if (stationInfoSrc
== null || stationTemplate
== null) {
583 'Missing charging station template or existing configuration to propagate serial number'
586 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
587 stationInfoSrc
.chargePointSerialNumber
!= null
588 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
589 : stationInfoDst
.chargePointSerialNumber
!= null &&
590 delete stationInfoDst
.chargePointSerialNumber
591 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
592 stationInfoSrc
.chargeBoxSerialNumber
!= null
593 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
594 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
595 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
596 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
597 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
600 export const hasFeatureProfile
= (
601 chargingStation
: ChargingStation
,
602 featureProfile
: SupportedFeatureProfiles
603 ): boolean | undefined => {
604 return getConfigurationKey(
606 StandardParametersKey
.SupportedFeatureProfiles
607 )?.value
?.includes(featureProfile
)
610 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
612 switch (stationInfo
.amperageLimitationUnit
) {
613 case AmpereUnits
.DECI_AMPERE
:
616 case AmpereUnits
.CENTI_AMPERE
:
619 case AmpereUnits
.MILLI_AMPERE
:
627 * Gets the connector cloned charging profiles applying a power limitation
628 * and sorted by connector id descending then stack level descending
630 * @param chargingStation -
631 * @param connectorId -
632 * @returns connector charging profiles array
634 export const getConnectorChargingProfiles
= (
635 chargingStation
: ChargingStation
,
637 ): ChargingProfile
[] => {
638 return clone
<ChargingProfile
[]>(
639 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
640 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
642 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
643 (a
, b
) => b
.stackLevel
- a
.stackLevel
649 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
650 chargingStation
: ChargingStation
,
652 ): number | undefined => {
653 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
654 // Get charging profiles sorted by connector id then stack level
655 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
656 if (isNotEmptyArray(chargingProfiles
)) {
657 const result
= getLimitFromChargingProfiles(
661 chargingStation
.logPrefix()
663 if (result
!= null) {
665 chargingProfile
= result
.chargingProfile
666 switch (chargingStation
.stationInfo
?.currentOutType
) {
669 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
671 : ACElectricUtils
.powerTotal(
672 chargingStation
.getNumberOfPhases(),
673 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
674 chargingStation
.stationInfo
.voltageOut
!,
680 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
682 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
683 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
685 const connectorMaximumPower
=
686 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
687 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
688 if (limit
> connectorMaximumPower
) {
690 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
691 chargingProfile.chargingProfileId
692 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
695 limit
= connectorMaximumPower
702 export const getDefaultVoltageOut
= (
703 currentType
: CurrentType
,
707 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
708 let defaultVoltageOut
: number
709 switch (currentType
) {
711 defaultVoltageOut
= Voltage
.VOLTAGE_230
714 defaultVoltageOut
= Voltage
.VOLTAGE_400
717 logger
.error(`${logPrefix} ${errorMsg}`)
718 throw new BaseError(errorMsg
)
720 return defaultVoltageOut
723 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
724 return stationInfo
.idTagsFile
!= null
725 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
729 export const waitChargingStationEvents
= async (
730 emitter
: EventEmitter
,
731 event
: ChargingStationWorkerMessageEvents
,
733 ): Promise
<number> => {
734 return await new Promise
<number>(resolve
=> {
736 if (eventsToWait
=== 0) {
740 emitter
.on(event
, () => {
742 if (events
=== eventsToWait
) {
749 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
750 let configuredMaxNumberOfConnectors
= 0
751 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
752 const numberOfConnectors
= stationTemplate
.numberOfConnectors
753 configuredMaxNumberOfConnectors
=
754 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
755 } else if (stationTemplate
.numberOfConnectors
!= null) {
756 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
757 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
758 configuredMaxNumberOfConnectors
=
759 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
760 stationTemplate
.Connectors
[0] != null
761 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
762 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
763 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
764 for (const evse
in stationTemplate
.Evses
) {
768 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
769 stationTemplate
.Evses
[evse
].Connectors
773 return configuredMaxNumberOfConnectors
776 const checkConfiguredMaxConnectors
= (
777 configuredMaxConnectors
: number,
781 if (configuredMaxConnectors
<= 0) {
783 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
788 const checkTemplateMaxConnectors
= (
789 templateMaxConnectors
: number,
793 if (templateMaxConnectors
=== 0) {
795 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
797 } else if (templateMaxConnectors
< 0) {
799 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
804 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
805 connectorStatus
.availability
= AvailabilityType
.Operative
806 connectorStatus
.idTagLocalAuthorized
= false
807 connectorStatus
.idTagAuthorized
= false
808 connectorStatus
.transactionRemoteStarted
= false
809 connectorStatus
.transactionStarted
= false
810 connectorStatus
.energyActiveImportRegisterValue
= 0
811 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
812 if (connectorStatus
.chargingProfiles
== null) {
813 connectorStatus
.chargingProfiles
= []
817 const warnDeprecatedTemplateKey
= (
818 template
: ChargingStationTemplate
,
821 templateFile
: string,
824 if (template
[key
as keyof ChargingStationTemplate
] != null) {
825 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
826 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
828 logger
.warn(`${logPrefix} ${logMsg}`)
829 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
833 const convertDeprecatedTemplateKey
= (
834 template
: ChargingStationTemplate
,
835 deprecatedKey
: string,
838 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
840 (template
as unknown
as Record
<string, unknown
>)[key
] =
841 template
[deprecatedKey
as keyof ChargingStationTemplate
]
843 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
844 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
848 interface ChargingProfilesLimit
{
850 chargingProfile
: ChargingProfile
854 * Charging profiles shall already be sorted by connector id descending then stack level descending
856 * @param chargingStation -
857 * @param connectorId -
858 * @param chargingProfiles -
860 * @returns ChargingProfilesLimit
862 const getLimitFromChargingProfiles
= (
863 chargingStation
: ChargingStation
,
865 chargingProfiles
: ChargingProfile
[],
867 ): ChargingProfilesLimit
| undefined => {
868 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
869 const currentDate
= new Date()
870 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
871 for (const chargingProfile
of chargingProfiles
) {
872 const chargingSchedule
= chargingProfile
.chargingSchedule
873 if (chargingSchedule
.startSchedule
== null) {
875 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
877 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
878 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
880 if (!isDate(chargingSchedule
.startSchedule
)) {
882 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
884 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
885 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
887 if (chargingSchedule
.duration
== null) {
889 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
891 // OCPP specifies that if duration is not defined, it should be infinite
892 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
894 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
897 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
900 // Check if the charging profile is active
902 isWithinInterval(currentDate
, {
903 start
: chargingSchedule
.startSchedule
,
904 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
907 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
908 const chargingSchedulePeriodCompareFn
= (
909 a
: ChargingSchedulePeriod
,
910 b
: ChargingSchedulePeriod
911 ): number => a
.startPeriod
- b
.startPeriod
913 !isArraySorted
<ChargingSchedulePeriod
>(
914 chargingSchedule
.chargingSchedulePeriod
,
915 chargingSchedulePeriodCompareFn
919 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
921 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
923 // Check if the first schedule period startPeriod property is equal to 0
924 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
926 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
930 // Handle only one schedule period
931 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
932 const result
: ChargingProfilesLimit
= {
933 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
936 logger
.debug(debugLogMsg
, result
)
939 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
940 // Search for the right schedule period
943 chargingSchedulePeriod
944 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
945 // Find the right schedule period
948 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
952 // Found the schedule period: previous is the correct one
953 const result
: ChargingProfilesLimit
= {
954 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
955 limit
: previousChargingSchedulePeriod
!.limit
,
958 logger
.debug(debugLogMsg
, result
)
961 // Keep a reference to previous one
962 previousChargingSchedulePeriod
= chargingSchedulePeriod
963 // Handle the last schedule period within the charging profile duration
965 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
966 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
969 chargingSchedule
.startSchedule
,
970 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
972 chargingSchedule
.startSchedule
973 ) > chargingSchedule
.duration
)
975 const result
: ChargingProfilesLimit
= {
976 limit
: previousChargingSchedulePeriod
.limit
,
979 logger
.debug(debugLogMsg
, result
)
988 export const prepareChargingProfileKind
= (
989 connectorStatus
: ConnectorStatus
| undefined,
990 chargingProfile
: ChargingProfile
,
991 currentDate
: string | number | Date,
994 switch (chargingProfile
.chargingProfileKind
) {
995 case ChargingProfileKindType
.RECURRING
:
996 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
999 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
1001 case ChargingProfileKindType
.RELATIVE
:
1002 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
1004 `${logPrefix} ${moduleName}.prepareChargingProfileKind: Relative charging profile id ${chargingProfile.chargingProfileId} has a startSchedule property defined. It will be ignored or used if the connector has a transaction started`
1006 delete chargingProfile
.chargingSchedule
.startSchedule
1008 if (connectorStatus
?.transactionStarted
=== true) {
1009 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
1011 // FIXME: handle relative charging profile duration
1017 export const canProceedChargingProfile
= (
1018 chargingProfile
: ChargingProfile
,
1019 currentDate
: string | number | Date,
1023 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1024 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1027 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1028 chargingProfile.chargingProfileId
1029 } is not valid for the current date ${
1030 isDate(currentDate) ? currentDate.toISOString() : currentDate
1036 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1037 chargingProfile
.chargingSchedule
.duration
== null
1040 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1044 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1046 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1050 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1052 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1059 const canProceedRecurringChargingProfile
= (
1060 chargingProfile
: ChargingProfile
,
1064 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1065 chargingProfile
.recurrencyKind
== null
1068 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1073 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1074 chargingProfile
.chargingSchedule
.startSchedule
== null
1077 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1085 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1087 * @param chargingProfile -
1088 * @param currentDate -
1089 * @param logPrefix -
1091 const prepareRecurringChargingProfile
= (
1092 chargingProfile
: ChargingProfile
,
1093 currentDate
: string | number | Date,
1096 const chargingSchedule
= chargingProfile
.chargingSchedule
1097 let recurringIntervalTranslated
= false
1098 let recurringInterval
: Interval
| undefined
1099 switch (chargingProfile
.recurrencyKind
) {
1100 case RecurrencyKindType
.DAILY
:
1101 recurringInterval
= {
1102 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1103 start
: chargingSchedule
.startSchedule
!,
1104 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1105 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1107 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1109 !isWithinInterval(currentDate
, recurringInterval
) &&
1110 isBefore(recurringInterval
.end
, currentDate
)
1112 chargingSchedule
.startSchedule
= addDays(
1113 recurringInterval
.start
,
1114 differenceInDays(currentDate
, recurringInterval
.start
)
1116 recurringInterval
= {
1117 start
: chargingSchedule
.startSchedule
,
1118 end
: addDays(chargingSchedule
.startSchedule
, 1)
1120 recurringIntervalTranslated
= true
1123 case RecurrencyKindType
.WEEKLY
:
1124 recurringInterval
= {
1125 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1126 start
: chargingSchedule
.startSchedule
!,
1127 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1128 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1130 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1132 !isWithinInterval(currentDate
, recurringInterval
) &&
1133 isBefore(recurringInterval
.end
, currentDate
)
1135 chargingSchedule
.startSchedule
= addWeeks(
1136 recurringInterval
.start
,
1137 differenceInWeeks(currentDate
, recurringInterval
.start
)
1139 recurringInterval
= {
1140 start
: chargingSchedule
.startSchedule
,
1141 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1143 recurringIntervalTranslated
= true
1148 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1151 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1152 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1154 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1155 chargingProfile.recurrencyKind
1156 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1157 recurringInterval?.start as Date
1158 ).toISOString()}, ${toDate(
1159 recurringInterval?.end as Date
1160 ).toISOString()}] has not been properly translated to current date ${
1161 isDate(currentDate) ? currentDate.toISOString() : currentDate
1165 return recurringIntervalTranslated
1168 const checkRecurringChargingProfileDuration
= (
1169 chargingProfile
: ChargingProfile
,
1173 if (chargingProfile
.chargingSchedule
.duration
== null) {
1175 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1176 chargingProfile.chargingProfileKind
1177 } charging profile id ${
1178 chargingProfile.chargingProfileId
1179 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1184 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1186 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1189 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1190 chargingProfile.chargingProfileKind
1191 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1192 chargingProfile.chargingSchedule.duration
1193 } is greater than the recurrency time interval duration ${differenceInSeconds(
1198 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1202 const getRandomSerialNumberSuffix
= (params
?: {
1203 randomBytesLength
?: number
1206 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1207 if (params
?.upperCase
=== true) {
1208 return randomSerialNumberSuffix
.toUpperCase()
1210 return randomSerialNumberSuffix