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'
73 } from
'../utils/index.js'
75 const moduleName
= 'Helpers'
77 export const getChargingStationId
= (
79 stationTemplate
: ChargingStationTemplate
| undefined
81 if (stationTemplate
=== undefined) {
82 return "Unknown 'chargingStationId'"
84 // In case of multiple instances: add instance index to charging station id
85 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
86 const idSuffix
= stationTemplate
?.nameSuffix
?? ''
87 const idStr
= `000000000${index.toString()}`
88 return stationTemplate
?.fixedName
!= null
89 ? stationTemplate
.baseName
90 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
95 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
96 return isPast(reservation
.expiryDate
)
99 export const removeExpiredReservations
= async (
100 chargingStation
: ChargingStation
101 ): Promise
<void> => {
102 if (chargingStation
.hasEvses
) {
103 for (const evseStatus
of chargingStation
.evses
.values()) {
104 for (const connectorStatus
of evseStatus
.connectors
.values()) {
106 connectorStatus
.reservation
!= null &&
107 hasReservationExpired(connectorStatus
.reservation
)
109 await chargingStation
.removeReservation(
110 connectorStatus
.reservation
,
111 ReservationTerminationReason
.EXPIRED
117 for (const connectorStatus
of chargingStation
.connectors
.values()) {
119 connectorStatus
.reservation
!= null &&
120 hasReservationExpired(connectorStatus
.reservation
)
122 await chargingStation
.removeReservation(
123 connectorStatus
.reservation
,
124 ReservationTerminationReason
.EXPIRED
131 export const getNumberOfReservableConnectors
= (
132 connectors
: Map
<number, ConnectorStatus
>
134 let numberOfReservableConnectors
= 0
135 for (const [connectorId
, connectorStatus
] of connectors
) {
136 if (connectorId
=== 0) {
139 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
140 ++numberOfReservableConnectors
143 return numberOfReservableConnectors
146 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
147 const chargingStationInfo
= {
148 chargePointModel
: stationTemplate
.chargePointModel
,
149 chargePointVendor
: stationTemplate
.chargePointVendor
,
150 ...(!isUndefined(stationTemplate
.chargeBoxSerialNumberPrefix
) && {
151 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
153 ...(!isUndefined(stationTemplate
.chargePointSerialNumberPrefix
) && {
154 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
156 ...(!isUndefined(stationTemplate
.meterSerialNumberPrefix
) && {
157 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
159 ...(!isUndefined(stationTemplate
.meterType
) && {
160 meterType
: stationTemplate
.meterType
163 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
164 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
168 export const checkChargingStation
= (
169 chargingStation
: ChargingStation
,
172 if (!chargingStation
.started
&& !chargingStation
.starting
) {
173 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
179 export const getPhaseRotationValue
= (
181 numberOfPhases
: number
182 ): string | undefined => {
184 if (connectorId
=== 0 && numberOfPhases
=== 0) {
185 return `${connectorId}.${ConnectorPhaseRotation.RST}`
186 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
187 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
189 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
190 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
191 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
192 return `${connectorId}.${ConnectorPhaseRotation.RST}`
196 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
>): number => {
200 return Object.keys(evses
).length
203 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
204 if (connectors
== null) {
207 return Object.keys(connectors
).length
210 export const getBootConnectorStatus
= (
211 chargingStation
: ChargingStation
,
213 connectorStatus
: ConnectorStatus
214 ): ConnectorStatusEnum
=> {
215 let connectorBootStatus
: ConnectorStatusEnum
217 connectorStatus
?.status == null &&
218 (!chargingStation
.isChargingStationAvailable() ||
219 !chargingStation
.isConnectorAvailable(connectorId
))
221 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
222 } else if (connectorStatus
?.status == null && connectorStatus
?.bootStatus
!= null) {
223 // Set boot status in template at startup
224 connectorBootStatus
= connectorStatus
?.bootStatus
225 } else if (connectorStatus
?.status != null) {
226 // Set previous status at startup
227 connectorBootStatus
= connectorStatus
?.status
229 // Set default status
230 connectorBootStatus
= ConnectorStatusEnum
.Available
232 return connectorBootStatus
235 export const checkTemplate
= (
236 stationTemplate
: ChargingStationTemplate
,
240 if (stationTemplate
== null) {
241 const errorMsg
= `Failed to read charging station template file ${templateFile}`
242 logger
.error(`${logPrefix} ${errorMsg}`)
243 throw new BaseError(errorMsg
)
245 if (isEmptyObject(stationTemplate
)) {
246 const errorMsg
= `Empty charging station information from template file ${templateFile}`
247 logger
.error(`${logPrefix} ${errorMsg}`)
248 throw new BaseError(errorMsg
)
250 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
251 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
252 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
254 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
255 Constants
.DEFAULT_ATG_CONFIGURATION
258 if (isNullOrUndefined(stationTemplate
.idTagsFile
) || isEmptyString(stationTemplate
.idTagsFile
)) {
260 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
265 export const checkConfiguration
= (
266 stationConfiguration
: ChargingStationConfiguration
| undefined,
268 configurationFile
: string
270 if (isNullOrUndefined(stationConfiguration
)) {
271 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
272 logger
.error(`${logPrefix} ${errorMsg}`)
273 throw new BaseError(errorMsg
)
275 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
276 if (isEmptyObject(stationConfiguration
!)) {
277 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
278 logger
.error(`${logPrefix} ${errorMsg}`)
279 throw new BaseError(errorMsg
)
283 export const checkConnectorsConfiguration
= (
284 stationTemplate
: ChargingStationTemplate
,
288 configuredMaxConnectors
: number
289 templateMaxConnectors
: number
290 templateMaxAvailableConnectors
: number
292 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
293 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
294 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
295 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!)
296 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
297 const templateMaxAvailableConnectors
=
298 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
300 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
301 stationTemplate
?.randomConnectors
=== false
304 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
306 stationTemplate
.randomConnectors
= true
308 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
}
311 export const checkStationInfoConnectorStatus
= (
313 connectorStatus
: ConnectorStatus
,
317 if (!isNullOrUndefined(connectorStatus
?.status)) {
319 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
321 delete connectorStatus
.status
325 export const buildConnectorsMap
= (
326 connectors
: Record
<string, ConnectorStatus
>,
329 ): Map
<number, ConnectorStatus
> => {
330 const connectorsMap
= new Map
<number, ConnectorStatus
>()
331 if (getMaxNumberOfConnectors(connectors
) > 0) {
332 for (const connector
in connectors
) {
333 const connectorStatus
= connectors
[connector
]
334 const connectorId
= convertToInt(connector
)
335 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
336 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
))
340 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
346 export const initializeConnectorsMapStatus
= (
347 connectors
: Map
<number, ConnectorStatus
>,
350 for (const connectorId
of connectors
.keys()) {
351 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
353 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
358 if (connectorId
=== 0) {
359 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
360 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
361 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
362 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
363 connectors
.get(connectorId
)!.chargingProfiles
= []
367 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
369 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
370 initializeConnectorStatus(connectors
.get(connectorId
)!)
375 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
376 connectorStatus
.chargingProfiles
=
377 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
378 ? connectorStatus
.chargingProfiles
?.filter(
379 (chargingProfile
) => chargingProfile
.transactionId
!== connectorStatus
.transactionId
382 connectorStatus
.idTagLocalAuthorized
= false
383 connectorStatus
.idTagAuthorized
= false
384 connectorStatus
.transactionRemoteStarted
= false
385 connectorStatus
.transactionStarted
= false
386 delete connectorStatus
?.transactionStart
387 delete connectorStatus
?.transactionId
388 delete connectorStatus
?.localAuthorizeIdTag
389 delete connectorStatus
?.authorizeIdTag
390 delete connectorStatus
?.transactionIdTag
391 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
392 delete connectorStatus
?.transactionBeginMeterValue
395 export const createBootNotificationRequest
= (
396 stationInfo
: ChargingStationInfo
,
397 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
398 ): BootNotificationRequest
=> {
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 const ocppVersion
= stationInfo
.ocppVersion
!
401 switch (ocppVersion
) {
402 case OCPPVersion
.VERSION_16
:
404 chargePointModel
: stationInfo
.chargePointModel
,
405 chargePointVendor
: stationInfo
.chargePointVendor
,
406 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
407 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
409 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
410 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
412 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
413 firmwareVersion
: stationInfo
.firmwareVersion
415 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
416 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
417 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
418 meterSerialNumber
: stationInfo
.meterSerialNumber
420 ...(!isUndefined(stationInfo
.meterType
) && {
421 meterType
: stationInfo
.meterType
423 } satisfies OCPP16BootNotificationRequest
424 case OCPPVersion
.VERSION_20
:
425 case OCPPVersion
.VERSION_201
:
429 model
: stationInfo
.chargePointModel
,
430 vendorName
: stationInfo
.chargePointVendor
,
431 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
432 firmwareVersion
: stationInfo
.firmwareVersion
434 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
435 serialNumber
: stationInfo
.chargeBoxSerialNumber
437 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
439 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
440 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
})
444 } satisfies OCPP20BootNotificationRequest
448 export const warnTemplateKeysDeprecation
= (
449 stationTemplate
: ChargingStationTemplate
,
453 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
454 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
455 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
456 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
457 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
459 for (const templateKey
of templateKeys
) {
460 warnDeprecatedTemplateKey(
462 templateKey
.deprecatedKey
,
465 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined
467 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
471 export const stationTemplateToStationInfo
= (
472 stationTemplate
: ChargingStationTemplate
473 ): ChargingStationInfo
=> {
474 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
)
475 delete stationTemplate
.power
476 delete stationTemplate
.powerUnit
477 delete stationTemplate
.Connectors
478 delete stationTemplate
.Evses
479 delete stationTemplate
.Configuration
480 delete stationTemplate
.AutomaticTransactionGenerator
481 delete stationTemplate
.chargeBoxSerialNumberPrefix
482 delete stationTemplate
.chargePointSerialNumberPrefix
483 delete stationTemplate
.meterSerialNumberPrefix
484 return stationTemplate
as ChargingStationInfo
487 export const createSerialNumber
= (
488 stationTemplate
: ChargingStationTemplate
,
489 stationInfo
: ChargingStationInfo
,
491 randomSerialNumberUpperCase
?: boolean
492 randomSerialNumber
?: boolean
495 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
496 const serialNumberSuffix
=
497 params
?.randomSerialNumber
=== true
498 ? getRandomSerialNumberSuffix({
499 upperCase
: params
.randomSerialNumberUpperCase
502 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
503 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
504 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
505 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
506 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
507 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
510 export const propagateSerialNumber
= (
511 stationTemplate
: ChargingStationTemplate
,
512 stationInfoSrc
: ChargingStationInfo
,
513 stationInfoDst
: ChargingStationInfo
515 if (stationInfoSrc
== null || stationTemplate
== null) {
517 'Missing charging station template or existing configuration to propagate serial number'
520 stationTemplate
?.chargePointSerialNumberPrefix
!= null &&
521 stationInfoSrc
?.chargePointSerialNumber
!= null
522 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
523 : stationInfoDst
?.chargePointSerialNumber
!= null &&
524 delete stationInfoDst
.chargePointSerialNumber
525 stationTemplate
?.chargeBoxSerialNumberPrefix
!= null &&
526 stationInfoSrc
?.chargeBoxSerialNumber
!= null
527 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
528 : stationInfoDst
?.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
529 stationTemplate
?.meterSerialNumberPrefix
!= null && stationInfoSrc
?.meterSerialNumber
!= null
530 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
531 : stationInfoDst
?.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
534 export const hasFeatureProfile
= (
535 chargingStation
: ChargingStation
,
536 featureProfile
: SupportedFeatureProfiles
537 ): boolean | undefined => {
538 return getConfigurationKey(
540 StandardParametersKey
.SupportedFeatureProfiles
541 )?.value
?.includes(featureProfile
)
544 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
546 switch (stationInfo
.amperageLimitationUnit
) {
547 case AmpereUnits
.DECI_AMPERE
:
550 case AmpereUnits
.CENTI_AMPERE
:
553 case AmpereUnits
.MILLI_AMPERE
:
561 * Gets the connector cloned charging profiles applying a power limitation
562 * and sorted by connector id descending then stack level descending
564 * @param chargingStation -
565 * @param connectorId -
566 * @returns connector charging profiles array
568 export const getConnectorChargingProfiles
= (
569 chargingStation
: ChargingStation
,
571 ): ChargingProfile
[] => {
572 return cloneObject
<ChargingProfile
[]>(
573 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
574 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
576 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
577 (a
, b
) => b
.stackLevel
- a
.stackLevel
583 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
584 chargingStation
: ChargingStation
,
586 ): number | undefined => {
587 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
588 // Get charging profiles sorted by connector id then stack level
589 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
590 if (isNotEmptyArray(chargingProfiles
)) {
591 const result
= getLimitFromChargingProfiles(
595 chargingStation
.logPrefix()
597 if (!isNullOrUndefined(result
)) {
598 limit
= result
?.limit
599 chargingProfile
= result
?.chargingProfile
600 switch (chargingStation
.stationInfo
?.currentOutType
) {
603 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
605 : ACElectricUtils
.powerTotal(
606 chargingStation
.getNumberOfPhases(),
607 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
608 chargingStation
.stationInfo
.voltageOut
!,
609 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
615 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
617 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
618 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
!)
620 const connectorMaximumPower
=
621 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
622 chargingStation
.stationInfo
.maximumPower
! / chargingStation
.powerDivider
623 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
624 if (limit
! > connectorMaximumPower
) {
626 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
629 limit
= connectorMaximumPower
636 export const getDefaultVoltageOut
= (
637 currentType
: CurrentType
,
641 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
642 let defaultVoltageOut
: number
643 switch (currentType
) {
645 defaultVoltageOut
= Voltage
.VOLTAGE_230
648 defaultVoltageOut
= Voltage
.VOLTAGE_400
651 logger
.error(`${logPrefix} ${errorMsg}`)
652 throw new BaseError(errorMsg
)
654 return defaultVoltageOut
657 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
658 return stationInfo
.idTagsFile
!= null
659 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
663 export const waitChargingStationEvents
= async (
664 emitter
: EventEmitter
,
665 event
: ChargingStationWorkerMessageEvents
,
667 ): Promise
<number> => {
668 return await new Promise
<number>((resolve
) => {
670 if (eventsToWait
=== 0) {
674 emitter
.on(event
, () => {
676 if (events
=== eventsToWait
) {
683 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
684 let configuredMaxNumberOfConnectors
= 0
685 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
686 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[]
687 configuredMaxNumberOfConnectors
=
688 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
689 } else if (!isUndefined(stationTemplate
.numberOfConnectors
)) {
690 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number
691 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
692 configuredMaxNumberOfConnectors
=
693 stationTemplate
.Connectors
?.[0] != null
694 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
695 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
696 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
697 for (const evse
in stationTemplate
.Evses
) {
701 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
702 stationTemplate
.Evses
[evse
].Connectors
706 return configuredMaxNumberOfConnectors
709 const checkConfiguredMaxConnectors
= (
710 configuredMaxConnectors
: number,
714 if (configuredMaxConnectors
<= 0) {
716 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
721 const checkTemplateMaxConnectors
= (
722 templateMaxConnectors
: number,
726 if (templateMaxConnectors
=== 0) {
728 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
730 } else if (templateMaxConnectors
< 0) {
732 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
737 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
738 connectorStatus
.availability
= AvailabilityType
.Operative
739 connectorStatus
.idTagLocalAuthorized
= false
740 connectorStatus
.idTagAuthorized
= false
741 connectorStatus
.transactionRemoteStarted
= false
742 connectorStatus
.transactionStarted
= false
743 connectorStatus
.energyActiveImportRegisterValue
= 0
744 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
745 if (isUndefined(connectorStatus
.chargingProfiles
)) {
746 connectorStatus
.chargingProfiles
= []
750 const warnDeprecatedTemplateKey
= (
751 template
: ChargingStationTemplate
,
754 templateFile
: string,
757 if (!isUndefined(template
?.[key
as keyof ChargingStationTemplate
])) {
758 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
759 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
761 logger
.warn(`${logPrefix} ${logMsg}`)
762 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
766 const convertDeprecatedTemplateKey
= (
767 template
: ChargingStationTemplate
,
768 deprecatedKey
: string,
771 if (!isUndefined(template
?.[deprecatedKey
as keyof ChargingStationTemplate
])) {
772 if (!isUndefined(key
)) {
773 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
774 (template
as unknown
as Record
<string, unknown
>)[key
!] =
775 template
[deprecatedKey
as keyof ChargingStationTemplate
]
777 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
778 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
782 interface ChargingProfilesLimit
{
784 chargingProfile
: ChargingProfile
788 * Charging profiles shall already be sorted by connector id descending then stack level descending
790 * @param chargingStation -
791 * @param connectorId -
792 * @param chargingProfiles -
794 * @returns ChargingProfilesLimit
796 const getLimitFromChargingProfiles
= (
797 chargingStation
: ChargingStation
,
799 chargingProfiles
: ChargingProfile
[],
801 ): ChargingProfilesLimit
| undefined => {
802 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
803 const currentDate
= new Date()
804 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
805 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!
806 for (const chargingProfile
of chargingProfiles
) {
807 const chargingSchedule
= chargingProfile
.chargingSchedule
809 isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
810 connectorStatus
?.transactionStarted
=== true
813 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
815 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
816 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
819 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
820 !isDate(chargingSchedule
?.startSchedule
)
823 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
825 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
826 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!
829 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
830 isNullOrUndefined(chargingSchedule
?.duration
)
833 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
835 // OCPP specifies that if duration is not defined, it should be infinite
836 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
837 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
!)
839 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
842 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
845 // Check if the charging profile is active
847 isWithinInterval(currentDate
, {
848 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
849 start
: chargingSchedule
.startSchedule
!,
850 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
851 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!)
854 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
855 const chargingSchedulePeriodCompareFn
= (
856 a
: ChargingSchedulePeriod
,
857 b
: ChargingSchedulePeriod
858 ): number => a
.startPeriod
- b
.startPeriod
860 !isArraySorted
<ChargingSchedulePeriod
>(
861 chargingSchedule
.chargingSchedulePeriod
,
862 chargingSchedulePeriodCompareFn
866 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
868 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
870 // Check if the first schedule period startPeriod property is equal to 0
871 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
873 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
877 // Handle only one schedule period
878 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
879 const result
: ChargingProfilesLimit
= {
880 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
883 logger
.debug(debugLogMsg
, result
)
886 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
887 // Search for the right schedule period
890 chargingSchedulePeriod
891 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
892 // Find the right schedule period
895 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
896 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
900 // Found the schedule period: previous is the correct one
901 const result
: ChargingProfilesLimit
= {
902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
903 limit
: previousChargingSchedulePeriod
!.limit
,
906 logger
.debug(debugLogMsg
, result
)
909 // Keep a reference to previous one
910 previousChargingSchedulePeriod
= chargingSchedulePeriod
911 // Handle the last schedule period within the charging profile duration
913 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
914 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
917 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
918 chargingSchedule
.startSchedule
!,
919 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
921 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
922 chargingSchedule
.startSchedule
!
923 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
924 ) > chargingSchedule
.duration
!)
926 const result
: ChargingProfilesLimit
= {
927 limit
: previousChargingSchedulePeriod
.limit
,
930 logger
.debug(debugLogMsg
, result
)
939 export const prepareChargingProfileKind
= (
940 connectorStatus
: ConnectorStatus
,
941 chargingProfile
: ChargingProfile
,
945 switch (chargingProfile
.chargingProfileKind
) {
946 case ChargingProfileKindType
.RECURRING
:
947 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
950 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
952 case ChargingProfileKindType
.RELATIVE
:
953 if (!isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)) {
955 `${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`
957 delete chargingProfile
.chargingSchedule
.startSchedule
959 if (connectorStatus
?.transactionStarted
=== true) {
960 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
962 // FIXME: Handle relative charging profile duration
968 export const canProceedChargingProfile
= (
969 chargingProfile
: ChargingProfile
,
974 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
975 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
976 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
977 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
980 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
981 chargingProfile.chargingProfileId
982 } is not valid for the current date ${currentDate.toISOString()}`
987 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) ||
988 isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)
991 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
996 !isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) &&
997 !isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)
1000 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1005 !isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
) &&
1006 !Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)
1009 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1016 const canProceedRecurringChargingProfile
= (
1017 chargingProfile
: ChargingProfile
,
1021 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1022 isNullOrUndefined(chargingProfile
.recurrencyKind
)
1025 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1030 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1031 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)
1034 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1042 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1044 * @param chargingProfile -
1045 * @param currentDate -
1046 * @param logPrefix -
1048 const prepareRecurringChargingProfile
= (
1049 chargingProfile
: ChargingProfile
,
1053 const chargingSchedule
= chargingProfile
.chargingSchedule
1054 let recurringIntervalTranslated
= false
1055 let recurringInterval
: Interval
1056 switch (chargingProfile
.recurrencyKind
) {
1057 case RecurrencyKindType
.DAILY
:
1058 recurringInterval
= {
1059 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1060 start
: chargingSchedule
.startSchedule
!,
1061 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1062 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1064 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1066 !isWithinInterval(currentDate
, recurringInterval
) &&
1067 isBefore(recurringInterval
.end
, currentDate
)
1069 chargingSchedule
.startSchedule
= addDays(
1070 recurringInterval
.start
,
1071 differenceInDays(currentDate
, recurringInterval
.start
)
1073 recurringInterval
= {
1074 start
: chargingSchedule
.startSchedule
,
1075 end
: addDays(chargingSchedule
.startSchedule
, 1)
1077 recurringIntervalTranslated
= true
1080 case RecurrencyKindType
.WEEKLY
:
1081 recurringInterval
= {
1082 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1083 start
: chargingSchedule
.startSchedule
!,
1084 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1085 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1087 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1089 !isWithinInterval(currentDate
, recurringInterval
) &&
1090 isBefore(recurringInterval
.end
, currentDate
)
1092 chargingSchedule
.startSchedule
= addWeeks(
1093 recurringInterval
.start
,
1094 differenceInWeeks(currentDate
, recurringInterval
.start
)
1096 recurringInterval
= {
1097 start
: chargingSchedule
.startSchedule
,
1098 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1100 recurringIntervalTranslated
= true
1105 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1108 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1109 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1111 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1112 chargingProfile.recurrencyKind
1113 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1114 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1115 recurringInterval!.start
1116 ).toISOString()}, ${toDate(
1117 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1118 recurringInterval!.end
1119 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
1122 return recurringIntervalTranslated
1125 const checkRecurringChargingProfileDuration
= (
1126 chargingProfile
: ChargingProfile
,
1130 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
1132 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1133 chargingProfile.chargingProfileKind
1134 } charging profile id ${
1135 chargingProfile.chargingProfileId
1136 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1141 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1143 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1144 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
1147 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1148 chargingProfile.chargingProfileKind
1149 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1150 chargingProfile.chargingSchedule.duration
1151 } is greater than the recurrency time interval duration ${differenceInSeconds(
1156 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1160 const getRandomSerialNumberSuffix
= (params
?: {
1161 randomBytesLength
?: number
1164 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1165 if (params
?.upperCase
=== true) {
1166 return randomSerialNumberSuffix
.toUpperCase()
1168 return randomSerialNumberSuffix