1 import { createHash
, randomBytes
} from
'node:crypto'
2 import type { EventEmitter
} from
'node:events'
3 import { basename
, dirname
, join
} 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'
25 import type { ChargingStation
} from
'./ChargingStation.js'
26 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
27 import { BaseError
} from
'../exception/index.js'
31 type BootNotificationRequest
,
34 ChargingProfileKindType
,
36 type ChargingSchedulePeriod
,
37 type ChargingStationConfiguration
,
38 type ChargingStationInfo
,
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'
72 } from
'../utils/index.js'
74 const moduleName
= 'Helpers'
76 export const getChargingStationId
= (
78 stationTemplate
: ChargingStationTemplate
| undefined
80 if (stationTemplate
== null) {
81 return "Unknown 'chargingStationId'"
83 // In case of multiple instances: add instance index to charging station id
84 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
85 const idSuffix
= stationTemplate
?.nameSuffix
?? ''
86 const idStr
= `000000000${index.toString()}`
87 return stationTemplate
?.fixedName
=== true
88 ? stationTemplate
.baseName
89 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
94 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
95 return isPast(reservation
.expiryDate
)
98 export const removeExpiredReservations
= async (
99 chargingStation
: ChargingStation
100 ): Promise
<void> => {
101 if (chargingStation
.hasEvses
) {
102 for (const evseStatus
of chargingStation
.evses
.values()) {
103 for (const connectorStatus
of evseStatus
.connectors
.values()) {
105 connectorStatus
.reservation
!= null &&
106 hasReservationExpired(connectorStatus
.reservation
)
108 await chargingStation
.removeReservation(
109 connectorStatus
.reservation
,
110 ReservationTerminationReason
.EXPIRED
116 for (const connectorStatus
of chargingStation
.connectors
.values()) {
118 connectorStatus
.reservation
!= null &&
119 hasReservationExpired(connectorStatus
.reservation
)
121 await chargingStation
.removeReservation(
122 connectorStatus
.reservation
,
123 ReservationTerminationReason
.EXPIRED
130 export const getNumberOfReservableConnectors
= (
131 connectors
: Map
<number, ConnectorStatus
>
133 let numberOfReservableConnectors
= 0
134 for (const [connectorId
, connectorStatus
] of connectors
) {
135 if (connectorId
=== 0) {
138 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
139 ++numberOfReservableConnectors
142 return numberOfReservableConnectors
145 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
146 const chargingStationInfo
= {
147 chargePointModel
: stationTemplate
.chargePointModel
,
148 chargePointVendor
: stationTemplate
.chargePointVendor
,
149 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
150 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
152 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
153 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
155 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
156 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
158 ...(!isUndefined(stationTemplate
.meterType
) && {
159 meterType
: stationTemplate
.meterType
162 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
163 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
167 export const checkChargingStation
= (
168 chargingStation
: ChargingStation
,
171 if (!chargingStation
.started
&& !chargingStation
.starting
) {
172 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
178 export const getPhaseRotationValue
= (
180 numberOfPhases
: number
181 ): string | undefined => {
183 if (connectorId
=== 0 && numberOfPhases
=== 0) {
184 return `${connectorId}.${ConnectorPhaseRotation.RST}`
185 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
186 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
188 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
189 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
190 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
191 return `${connectorId}.${ConnectorPhaseRotation.RST}`
195 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
199 return Object.keys(evses
).length
202 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
203 if (connectors
== null) {
206 return Object.keys(connectors
).length
209 export const getBootConnectorStatus
= (
210 chargingStation
: ChargingStation
,
212 connectorStatus
: ConnectorStatus
213 ): ConnectorStatusEnum
=> {
214 let connectorBootStatus
: ConnectorStatusEnum
216 connectorStatus
?.status == null &&
217 (!chargingStation
.isChargingStationAvailable() ||
218 !chargingStation
.isConnectorAvailable(connectorId
))
220 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
221 } else if (connectorStatus
?.status == null && connectorStatus
?.bootStatus
!= null) {
222 // Set boot status in template at startup
223 connectorBootStatus
= connectorStatus
?.bootStatus
224 } else if (connectorStatus
?.status != null) {
225 // Set previous status at startup
226 connectorBootStatus
= connectorStatus
?.status
228 // Set default status
229 connectorBootStatus
= ConnectorStatusEnum
.Available
231 return connectorBootStatus
234 export const checkTemplate
= (
235 stationTemplate
: ChargingStationTemplate
,
239 if (stationTemplate
== null) {
240 const errorMsg
= `Failed to read charging station template file ${templateFile}`
241 logger
.error(`${logPrefix} ${errorMsg}`)
242 throw new BaseError(errorMsg
)
244 if (isEmptyObject(stationTemplate
)) {
245 const errorMsg
= `Empty charging station information from template file ${templateFile}`
246 logger
.error(`${logPrefix} ${errorMsg}`)
247 throw new BaseError(errorMsg
)
249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
251 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
253 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
254 Constants
.DEFAULT_ATG_CONFIGURATION
257 if (stationTemplate
.idTagsFile
== null || isEmptyString(stationTemplate
.idTagsFile
)) {
259 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
264 export const checkConfiguration
= (
265 stationConfiguration
: ChargingStationConfiguration
| undefined,
267 configurationFile
: string
269 if (stationConfiguration
== null) {
270 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
271 logger
.error(`${logPrefix} ${errorMsg}`)
272 throw new BaseError(errorMsg
)
274 if (isEmptyObject(stationConfiguration
)) {
275 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
276 logger
.error(`${logPrefix} ${errorMsg}`)
277 throw new BaseError(errorMsg
)
281 export const checkConnectorsConfiguration
= (
282 stationTemplate
: ChargingStationTemplate
,
286 configuredMaxConnectors
: number
287 templateMaxConnectors
: number
288 templateMaxAvailableConnectors
: number
290 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
291 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
292 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
293 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!)
294 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
295 const templateMaxAvailableConnectors
=
296 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
298 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
299 stationTemplate
?.randomConnectors
=== false
302 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
304 stationTemplate
.randomConnectors
= true
306 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
}
309 export const checkStationInfoConnectorStatus
= (
311 connectorStatus
: ConnectorStatus
,
315 if (connectorStatus
?.status != null) {
317 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
319 delete connectorStatus
.status
323 export const buildConnectorsMap
= (
324 connectors
: Record
<string, ConnectorStatus
>,
327 ): Map
<number, ConnectorStatus
> => {
328 const connectorsMap
= new Map
<number, ConnectorStatus
>()
329 if (getMaxNumberOfConnectors(connectors
) > 0) {
330 for (const connector
in connectors
) {
331 const connectorStatus
= connectors
[connector
]
332 const connectorId
= convertToInt(connector
)
333 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
334 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
))
338 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
344 export const initializeConnectorsMapStatus
= (
345 connectors
: Map
<number, ConnectorStatus
>,
348 for (const connectorId
of connectors
.keys()) {
349 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
351 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
356 if (connectorId
=== 0) {
357 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
358 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
359 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
360 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
361 connectors
.get(connectorId
)!.chargingProfiles
= []
363 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
364 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
365 initializeConnectorStatus(connectors
.get(connectorId
)!)
370 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
371 connectorStatus
.chargingProfiles
=
372 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
373 ? connectorStatus
.chargingProfiles
?.filter(
374 (chargingProfile
) => chargingProfile
.transactionId
!== connectorStatus
.transactionId
377 connectorStatus
.idTagLocalAuthorized
= false
378 connectorStatus
.idTagAuthorized
= false
379 connectorStatus
.transactionRemoteStarted
= false
380 connectorStatus
.transactionStarted
= false
381 delete connectorStatus
?.transactionStart
382 delete connectorStatus
?.transactionId
383 delete connectorStatus
?.localAuthorizeIdTag
384 delete connectorStatus
?.authorizeIdTag
385 delete connectorStatus
?.transactionIdTag
386 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
387 delete connectorStatus
?.transactionBeginMeterValue
390 export const createBootNotificationRequest
= (
391 stationInfo
: ChargingStationInfo
,
392 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
393 ): BootNotificationRequest
=> {
394 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
395 const ocppVersion
= stationInfo
.ocppVersion
!
396 switch (ocppVersion
) {
397 case OCPPVersion
.VERSION_16
:
399 chargePointModel
: stationInfo
.chargePointModel
,
400 chargePointVendor
: stationInfo
.chargePointVendor
,
401 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
402 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
404 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
405 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
407 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
408 firmwareVersion
: stationInfo
.firmwareVersion
410 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
411 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
412 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
413 meterSerialNumber
: stationInfo
.meterSerialNumber
415 ...(!isUndefined(stationInfo
.meterType
) && {
416 meterType
: stationInfo
.meterType
418 } satisfies OCPP16BootNotificationRequest
419 case OCPPVersion
.VERSION_20
:
420 case OCPPVersion
.VERSION_201
:
424 model
: stationInfo
.chargePointModel
,
425 vendorName
: stationInfo
.chargePointVendor
,
426 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
427 firmwareVersion
: stationInfo
.firmwareVersion
429 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
430 serialNumber
: stationInfo
.chargeBoxSerialNumber
432 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
434 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
435 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
})
439 } satisfies OCPP20BootNotificationRequest
443 export const warnTemplateKeysDeprecation
= (
444 stationTemplate
: ChargingStationTemplate
,
448 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
449 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
450 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
451 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
452 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
454 for (const templateKey
of templateKeys
) {
455 warnDeprecatedTemplateKey(
457 templateKey
.deprecatedKey
,
460 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined
462 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
466 export const stationTemplateToStationInfo
= (
467 stationTemplate
: ChargingStationTemplate
468 ): ChargingStationInfo
=> {
469 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
)
470 delete stationTemplate
.power
471 delete stationTemplate
.powerUnit
472 delete stationTemplate
.Connectors
473 delete stationTemplate
.Evses
474 delete stationTemplate
.Configuration
475 delete stationTemplate
.AutomaticTransactionGenerator
476 delete stationTemplate
.chargeBoxSerialNumberPrefix
477 delete stationTemplate
.chargePointSerialNumberPrefix
478 delete stationTemplate
.meterSerialNumberPrefix
479 return stationTemplate
as ChargingStationInfo
482 export const createSerialNumber
= (
483 stationTemplate
: ChargingStationTemplate
,
484 stationInfo
: ChargingStationInfo
,
486 randomSerialNumberUpperCase
?: boolean
487 randomSerialNumber
?: boolean
490 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
491 const serialNumberSuffix
=
492 params
?.randomSerialNumber
=== true
493 ? getRandomSerialNumberSuffix({
494 upperCase
: params
.randomSerialNumberUpperCase
497 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
498 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
499 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
500 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
501 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
502 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
505 export const propagateSerialNumber
= (
506 stationTemplate
: ChargingStationTemplate
,
507 stationInfoSrc
: ChargingStationInfo
,
508 stationInfoDst
: ChargingStationInfo
510 if (stationInfoSrc
== null || stationTemplate
== null) {
512 'Missing charging station template or existing configuration to propagate serial number'
515 stationTemplate
?.chargePointSerialNumberPrefix
!= null &&
516 stationInfoSrc
?.chargePointSerialNumber
!= null
517 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
518 : stationInfoDst
?.chargePointSerialNumber
!= null &&
519 delete stationInfoDst
.chargePointSerialNumber
520 stationTemplate
?.chargeBoxSerialNumberPrefix
!= null &&
521 stationInfoSrc
?.chargeBoxSerialNumber
!= null
522 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
523 : stationInfoDst
?.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
524 stationTemplate
?.meterSerialNumberPrefix
!= null && stationInfoSrc
?.meterSerialNumber
!= null
525 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
526 : stationInfoDst
?.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
529 export const hasFeatureProfile
= (
530 chargingStation
: ChargingStation
,
531 featureProfile
: SupportedFeatureProfiles
532 ): boolean | undefined => {
533 return getConfigurationKey(
535 StandardParametersKey
.SupportedFeatureProfiles
536 )?.value
?.includes(featureProfile
)
539 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
541 switch (stationInfo
.amperageLimitationUnit
) {
542 case AmpereUnits
.DECI_AMPERE
:
545 case AmpereUnits
.CENTI_AMPERE
:
548 case AmpereUnits
.MILLI_AMPERE
:
556 * Gets the connector cloned charging profiles applying a power limitation
557 * and sorted by connector id descending then stack level descending
559 * @param chargingStation -
560 * @param connectorId -
561 * @returns connector charging profiles array
563 export const getConnectorChargingProfiles
= (
564 chargingStation
: ChargingStation
,
566 ): ChargingProfile
[] => {
567 return cloneObject
<ChargingProfile
[]>(
568 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
569 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
571 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
572 (a
, b
) => b
.stackLevel
- a
.stackLevel
578 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
579 chargingStation
: ChargingStation
,
581 ): number | undefined => {
582 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
583 // Get charging profiles sorted by connector id then stack level
584 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
585 if (isNotEmptyArray(chargingProfiles
)) {
586 const result
= getLimitFromChargingProfiles(
590 chargingStation
.logPrefix()
592 if (result
!= null) {
593 limit
= result
?.limit
594 chargingProfile
= result
?.chargingProfile
595 switch (chargingStation
.stationInfo
?.currentOutType
) {
598 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
600 : ACElectricUtils
.powerTotal(
601 chargingStation
.getNumberOfPhases(),
602 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
603 chargingStation
.stationInfo
.voltageOut
!,
609 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
611 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
612 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
614 const connectorMaximumPower
=
615 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
616 chargingStation
.stationInfo
.maximumPower
! / chargingStation
.powerDivider
617 if (limit
> connectorMaximumPower
) {
619 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
622 limit
= connectorMaximumPower
629 export const getDefaultVoltageOut
= (
630 currentType
: CurrentType
,
634 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
635 let defaultVoltageOut
: number
636 switch (currentType
) {
638 defaultVoltageOut
= Voltage
.VOLTAGE_230
641 defaultVoltageOut
= Voltage
.VOLTAGE_400
644 logger
.error(`${logPrefix} ${errorMsg}`)
645 throw new BaseError(errorMsg
)
647 return defaultVoltageOut
650 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
651 return stationInfo
.idTagsFile
!= null
652 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
656 export const waitChargingStationEvents
= async (
657 emitter
: EventEmitter
,
658 event
: ChargingStationWorkerMessageEvents
,
660 ): Promise
<number> => {
661 return await new Promise
<number>((resolve
) => {
663 if (eventsToWait
=== 0) {
667 emitter
.on(event
, () => {
669 if (events
=== eventsToWait
) {
676 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
677 let configuredMaxNumberOfConnectors
= 0
678 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
679 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[]
680 configuredMaxNumberOfConnectors
=
681 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
682 } else if (!isUndefined(stationTemplate
.numberOfConnectors
)) {
683 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number
684 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
685 configuredMaxNumberOfConnectors
=
686 stationTemplate
.Connectors
?.[0] != null
687 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
688 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
689 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
690 for (const evse
in stationTemplate
.Evses
) {
694 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
695 stationTemplate
.Evses
[evse
].Connectors
699 return configuredMaxNumberOfConnectors
702 const checkConfiguredMaxConnectors
= (
703 configuredMaxConnectors
: number,
707 if (configuredMaxConnectors
<= 0) {
709 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
714 const checkTemplateMaxConnectors
= (
715 templateMaxConnectors
: number,
719 if (templateMaxConnectors
=== 0) {
721 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
723 } else if (templateMaxConnectors
< 0) {
725 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
730 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
731 connectorStatus
.availability
= AvailabilityType
.Operative
732 connectorStatus
.idTagLocalAuthorized
= false
733 connectorStatus
.idTagAuthorized
= false
734 connectorStatus
.transactionRemoteStarted
= false
735 connectorStatus
.transactionStarted
= false
736 connectorStatus
.energyActiveImportRegisterValue
= 0
737 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
738 if (isUndefined(connectorStatus
.chargingProfiles
)) {
739 connectorStatus
.chargingProfiles
= []
743 const warnDeprecatedTemplateKey
= (
744 template
: ChargingStationTemplate
,
747 templateFile
: string,
750 if (!isUndefined(template
?.[key
as keyof ChargingStationTemplate
])) {
751 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
752 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
754 logger
.warn(`${logPrefix} ${logMsg}`)
755 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
759 const convertDeprecatedTemplateKey
= (
760 template
: ChargingStationTemplate
,
761 deprecatedKey
: string,
764 if (!isUndefined(template
?.[deprecatedKey
as keyof ChargingStationTemplate
])) {
765 if (!isUndefined(key
)) {
766 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
767 (template
as unknown
as Record
<string, unknown
>)[key
!] =
768 template
[deprecatedKey
as keyof ChargingStationTemplate
]
770 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
771 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
775 interface ChargingProfilesLimit
{
777 chargingProfile
: ChargingProfile
781 * Charging profiles shall already be sorted by connector id descending then stack level descending
783 * @param chargingStation -
784 * @param connectorId -
785 * @param chargingProfiles -
787 * @returns ChargingProfilesLimit
789 const getLimitFromChargingProfiles
= (
790 chargingStation
: ChargingStation
,
792 chargingProfiles
: ChargingProfile
[],
794 ): ChargingProfilesLimit
| undefined => {
795 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
796 const currentDate
= new Date()
797 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
798 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!
799 for (const chargingProfile
of chargingProfiles
) {
800 const chargingSchedule
= chargingProfile
.chargingSchedule
801 if (chargingSchedule
?.startSchedule
== null && connectorStatus
?.transactionStarted
=== true) {
803 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
805 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
806 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
808 if (chargingSchedule
?.startSchedule
!= null && !isDate(chargingSchedule
?.startSchedule
)) {
810 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
812 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
813 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!
815 if (chargingSchedule
?.startSchedule
!= null && chargingSchedule
?.duration
== null) {
817 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
819 // OCPP specifies that if duration is not defined, it should be infinite
820 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
822 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
825 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
828 // Check if the charging profile is active
830 isWithinInterval(currentDate
, {
831 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
832 start
: chargingSchedule
.startSchedule
!,
833 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
834 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!)
837 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
838 const chargingSchedulePeriodCompareFn
= (
839 a
: ChargingSchedulePeriod
,
840 b
: ChargingSchedulePeriod
841 ): number => a
.startPeriod
- b
.startPeriod
843 !isArraySorted
<ChargingSchedulePeriod
>(
844 chargingSchedule
.chargingSchedulePeriod
,
845 chargingSchedulePeriodCompareFn
849 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
851 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
853 // Check if the first schedule period startPeriod property is equal to 0
854 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
856 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
860 // Handle only one schedule period
861 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
862 const result
: ChargingProfilesLimit
= {
863 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
866 logger
.debug(debugLogMsg
, result
)
869 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
870 // Search for the right schedule period
873 chargingSchedulePeriod
874 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
875 // Find the right schedule period
878 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
879 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
883 // Found the schedule period: previous is the correct one
884 const result
: ChargingProfilesLimit
= {
885 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
886 limit
: previousChargingSchedulePeriod
!.limit
,
889 logger
.debug(debugLogMsg
, result
)
892 // Keep a reference to previous one
893 previousChargingSchedulePeriod
= chargingSchedulePeriod
894 // Handle the last schedule period within the charging profile duration
896 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
897 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
900 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
901 chargingSchedule
.startSchedule
!,
902 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
904 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
905 chargingSchedule
.startSchedule
!
906 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
907 ) > chargingSchedule
.duration
!)
909 const result
: ChargingProfilesLimit
= {
910 limit
: previousChargingSchedulePeriod
.limit
,
913 logger
.debug(debugLogMsg
, result
)
922 export const prepareChargingProfileKind
= (
923 connectorStatus
: ConnectorStatus
,
924 chargingProfile
: ChargingProfile
,
928 switch (chargingProfile
.chargingProfileKind
) {
929 case ChargingProfileKindType
.RECURRING
:
930 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
933 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
935 case ChargingProfileKindType
.RELATIVE
:
936 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
938 `${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`
940 delete chargingProfile
.chargingSchedule
.startSchedule
942 if (connectorStatus
?.transactionStarted
=== true) {
943 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
945 // FIXME: Handle relative charging profile duration
951 export const canProceedChargingProfile
= (
952 chargingProfile
: ChargingProfile
,
957 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
958 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
959 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
960 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
963 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
964 chargingProfile.chargingProfileId
965 } is not valid for the current date ${currentDate.toISOString()}`
970 chargingProfile
.chargingSchedule
.startSchedule
== null ||
971 chargingProfile
.chargingSchedule
.duration
== null
974 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
979 chargingProfile
.chargingSchedule
.startSchedule
!= null &&
980 !isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)
983 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
988 chargingProfile
.chargingSchedule
.duration
!= null &&
989 !Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)
992 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
999 const canProceedRecurringChargingProfile
= (
1000 chargingProfile
: ChargingProfile
,
1004 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1005 chargingProfile
.recurrencyKind
== null
1008 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1013 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1014 chargingProfile
.chargingSchedule
.startSchedule
== null
1017 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1025 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1027 * @param chargingProfile -
1028 * @param currentDate -
1029 * @param logPrefix -
1031 const prepareRecurringChargingProfile
= (
1032 chargingProfile
: ChargingProfile
,
1036 const chargingSchedule
= chargingProfile
.chargingSchedule
1037 let recurringIntervalTranslated
= false
1038 let recurringInterval
: Interval
1039 switch (chargingProfile
.recurrencyKind
) {
1040 case RecurrencyKindType
.DAILY
:
1041 recurringInterval
= {
1042 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1043 start
: chargingSchedule
.startSchedule
!,
1044 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1045 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1047 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1049 !isWithinInterval(currentDate
, recurringInterval
) &&
1050 isBefore(recurringInterval
.end
, currentDate
)
1052 chargingSchedule
.startSchedule
= addDays(
1053 recurringInterval
.start
,
1054 differenceInDays(currentDate
, recurringInterval
.start
)
1056 recurringInterval
= {
1057 start
: chargingSchedule
.startSchedule
,
1058 end
: addDays(chargingSchedule
.startSchedule
, 1)
1060 recurringIntervalTranslated
= true
1063 case RecurrencyKindType
.WEEKLY
:
1064 recurringInterval
= {
1065 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1066 start
: chargingSchedule
.startSchedule
!,
1067 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1068 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1070 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1072 !isWithinInterval(currentDate
, recurringInterval
) &&
1073 isBefore(recurringInterval
.end
, currentDate
)
1075 chargingSchedule
.startSchedule
= addWeeks(
1076 recurringInterval
.start
,
1077 differenceInWeeks(currentDate
, recurringInterval
.start
)
1079 recurringInterval
= {
1080 start
: chargingSchedule
.startSchedule
,
1081 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1083 recurringIntervalTranslated
= true
1088 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1091 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1092 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1094 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1095 chargingProfile.recurrencyKind
1096 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1097 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1098 recurringInterval!.start
1099 ).toISOString()}, ${toDate(
1100 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1101 recurringInterval!.end
1102 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
1105 return recurringIntervalTranslated
1108 const checkRecurringChargingProfileDuration
= (
1109 chargingProfile
: ChargingProfile
,
1113 if (chargingProfile
.chargingSchedule
.duration
== null) {
1115 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1116 chargingProfile.chargingProfileKind
1117 } charging profile id ${
1118 chargingProfile.chargingProfileId
1119 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1124 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1126 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1129 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1130 chargingProfile.chargingProfileKind
1131 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1132 chargingProfile.chargingSchedule.duration
1133 } is greater than the recurrency time interval duration ${differenceInSeconds(
1138 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1142 const getRandomSerialNumberSuffix
= (params
?: {
1143 randomBytesLength
?: number
1146 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1147 if (params
?.upperCase
=== true) {
1148 return randomSerialNumberSuffix
.toUpperCase()
1150 return randomSerialNumberSuffix