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
== null) {
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
=== true
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 (stationConfiguration
== null) {
271 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
272 logger
.error(`${logPrefix} ${errorMsg}`)
273 throw new BaseError(errorMsg
)
275 if (isEmptyObject(stationConfiguration
)) {
276 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
277 logger
.error(`${logPrefix} ${errorMsg}`)
278 throw new BaseError(errorMsg
)
282 export const checkConnectorsConfiguration
= (
283 stationTemplate
: ChargingStationTemplate
,
287 configuredMaxConnectors
: number
288 templateMaxConnectors
: number
289 templateMaxAvailableConnectors
: number
291 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
292 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
293 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
294 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!)
295 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
296 const templateMaxAvailableConnectors
=
297 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
299 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
300 stationTemplate
?.randomConnectors
=== false
303 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
305 stationTemplate
.randomConnectors
= true
307 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
}
310 export const checkStationInfoConnectorStatus
= (
312 connectorStatus
: ConnectorStatus
,
316 if (connectorStatus
?.status != null) {
318 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
320 delete connectorStatus
.status
324 export const buildConnectorsMap
= (
325 connectors
: Record
<string, ConnectorStatus
>,
328 ): Map
<number, ConnectorStatus
> => {
329 const connectorsMap
= new Map
<number, ConnectorStatus
>()
330 if (getMaxNumberOfConnectors(connectors
) > 0) {
331 for (const connector
in connectors
) {
332 const connectorStatus
= connectors
[connector
]
333 const connectorId
= convertToInt(connector
)
334 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
335 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
))
339 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
345 export const initializeConnectorsMapStatus
= (
346 connectors
: Map
<number, ConnectorStatus
>,
349 for (const connectorId
of connectors
.keys()) {
350 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
352 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
357 if (connectorId
=== 0) {
358 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
359 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
360 if (isUndefined(connectors
.get(connectorId
)?.chargingProfiles
)) {
361 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
362 connectors
.get(connectorId
)!.chargingProfiles
= []
366 isNullOrUndefined(connectors
.get(connectorId
)?.transactionStarted
)
368 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
369 initializeConnectorStatus(connectors
.get(connectorId
)!)
374 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
375 connectorStatus
.chargingProfiles
=
376 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
377 ? connectorStatus
.chargingProfiles
?.filter(
378 (chargingProfile
) => chargingProfile
.transactionId
!== connectorStatus
.transactionId
381 connectorStatus
.idTagLocalAuthorized
= false
382 connectorStatus
.idTagAuthorized
= false
383 connectorStatus
.transactionRemoteStarted
= false
384 connectorStatus
.transactionStarted
= false
385 delete connectorStatus
?.transactionStart
386 delete connectorStatus
?.transactionId
387 delete connectorStatus
?.localAuthorizeIdTag
388 delete connectorStatus
?.authorizeIdTag
389 delete connectorStatus
?.transactionIdTag
390 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
391 delete connectorStatus
?.transactionBeginMeterValue
394 export const createBootNotificationRequest
= (
395 stationInfo
: ChargingStationInfo
,
396 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
397 ): BootNotificationRequest
=> {
398 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
399 const ocppVersion
= stationInfo
.ocppVersion
!
400 switch (ocppVersion
) {
401 case OCPPVersion
.VERSION_16
:
403 chargePointModel
: stationInfo
.chargePointModel
,
404 chargePointVendor
: stationInfo
.chargePointVendor
,
405 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
406 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
408 ...(!isUndefined(stationInfo
.chargePointSerialNumber
) && {
409 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
411 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
412 firmwareVersion
: stationInfo
.firmwareVersion
414 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
415 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
}),
416 ...(!isUndefined(stationInfo
.meterSerialNumber
) && {
417 meterSerialNumber
: stationInfo
.meterSerialNumber
419 ...(!isUndefined(stationInfo
.meterType
) && {
420 meterType
: stationInfo
.meterType
422 } satisfies OCPP16BootNotificationRequest
423 case OCPPVersion
.VERSION_20
:
424 case OCPPVersion
.VERSION_201
:
428 model
: stationInfo
.chargePointModel
,
429 vendorName
: stationInfo
.chargePointVendor
,
430 ...(!isUndefined(stationInfo
.firmwareVersion
) && {
431 firmwareVersion
: stationInfo
.firmwareVersion
433 ...(!isUndefined(stationInfo
.chargeBoxSerialNumber
) && {
434 serialNumber
: stationInfo
.chargeBoxSerialNumber
436 ...((!isUndefined(stationInfo
.iccid
) || !isUndefined(stationInfo
.imsi
)) && {
438 ...(!isUndefined(stationInfo
.iccid
) && { iccid
: stationInfo
.iccid
}),
439 ...(!isUndefined(stationInfo
.imsi
) && { imsi
: stationInfo
.imsi
})
443 } satisfies OCPP20BootNotificationRequest
447 export const warnTemplateKeysDeprecation
= (
448 stationTemplate
: ChargingStationTemplate
,
452 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
453 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
454 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
455 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
456 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
458 for (const templateKey
of templateKeys
) {
459 warnDeprecatedTemplateKey(
461 templateKey
.deprecatedKey
,
464 !isUndefined(templateKey
.key
) ? `Use '${templateKey.key}' instead` : undefined
466 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
470 export const stationTemplateToStationInfo
= (
471 stationTemplate
: ChargingStationTemplate
472 ): ChargingStationInfo
=> {
473 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
)
474 delete stationTemplate
.power
475 delete stationTemplate
.powerUnit
476 delete stationTemplate
.Connectors
477 delete stationTemplate
.Evses
478 delete stationTemplate
.Configuration
479 delete stationTemplate
.AutomaticTransactionGenerator
480 delete stationTemplate
.chargeBoxSerialNumberPrefix
481 delete stationTemplate
.chargePointSerialNumberPrefix
482 delete stationTemplate
.meterSerialNumberPrefix
483 return stationTemplate
as ChargingStationInfo
486 export const createSerialNumber
= (
487 stationTemplate
: ChargingStationTemplate
,
488 stationInfo
: ChargingStationInfo
,
490 randomSerialNumberUpperCase
?: boolean
491 randomSerialNumber
?: boolean
494 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
495 const serialNumberSuffix
=
496 params
?.randomSerialNumber
=== true
497 ? getRandomSerialNumberSuffix({
498 upperCase
: params
.randomSerialNumberUpperCase
501 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
502 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
503 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
504 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
505 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
506 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
509 export const propagateSerialNumber
= (
510 stationTemplate
: ChargingStationTemplate
,
511 stationInfoSrc
: ChargingStationInfo
,
512 stationInfoDst
: ChargingStationInfo
514 if (stationInfoSrc
== null || stationTemplate
== null) {
516 'Missing charging station template or existing configuration to propagate serial number'
519 stationTemplate
?.chargePointSerialNumberPrefix
!= null &&
520 stationInfoSrc
?.chargePointSerialNumber
!= null
521 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
522 : stationInfoDst
?.chargePointSerialNumber
!= null &&
523 delete stationInfoDst
.chargePointSerialNumber
524 stationTemplate
?.chargeBoxSerialNumberPrefix
!= null &&
525 stationInfoSrc
?.chargeBoxSerialNumber
!= null
526 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
527 : stationInfoDst
?.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
528 stationTemplate
?.meterSerialNumberPrefix
!= null && stationInfoSrc
?.meterSerialNumber
!= null
529 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
530 : stationInfoDst
?.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
533 export const hasFeatureProfile
= (
534 chargingStation
: ChargingStation
,
535 featureProfile
: SupportedFeatureProfiles
536 ): boolean | undefined => {
537 return getConfigurationKey(
539 StandardParametersKey
.SupportedFeatureProfiles
540 )?.value
?.includes(featureProfile
)
543 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
545 switch (stationInfo
.amperageLimitationUnit
) {
546 case AmpereUnits
.DECI_AMPERE
:
549 case AmpereUnits
.CENTI_AMPERE
:
552 case AmpereUnits
.MILLI_AMPERE
:
560 * Gets the connector cloned charging profiles applying a power limitation
561 * and sorted by connector id descending then stack level descending
563 * @param chargingStation -
564 * @param connectorId -
565 * @returns connector charging profiles array
567 export const getConnectorChargingProfiles
= (
568 chargingStation
: ChargingStation
,
570 ): ChargingProfile
[] => {
571 return cloneObject
<ChargingProfile
[]>(
572 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
573 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
575 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
576 (a
, b
) => b
.stackLevel
- a
.stackLevel
582 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
583 chargingStation
: ChargingStation
,
585 ): number | undefined => {
586 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
587 // Get charging profiles sorted by connector id then stack level
588 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
589 if (isNotEmptyArray(chargingProfiles
)) {
590 const result
= getLimitFromChargingProfiles(
594 chargingStation
.logPrefix()
596 if (!isNullOrUndefined(result
)) {
597 limit
= result
?.limit
598 chargingProfile
= result
?.chargingProfile
599 switch (chargingStation
.stationInfo
?.currentOutType
) {
602 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
604 : ACElectricUtils
.powerTotal(
605 chargingStation
.getNumberOfPhases(),
606 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
607 chargingStation
.stationInfo
.voltageOut
!,
608 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
614 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
616 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
617 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
!)
619 const connectorMaximumPower
=
620 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
621 chargingStation
.stationInfo
.maximumPower
! / chargingStation
.powerDivider
622 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
623 if (limit
! > connectorMaximumPower
) {
625 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
628 limit
= connectorMaximumPower
635 export const getDefaultVoltageOut
= (
636 currentType
: CurrentType
,
640 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
641 let defaultVoltageOut
: number
642 switch (currentType
) {
644 defaultVoltageOut
= Voltage
.VOLTAGE_230
647 defaultVoltageOut
= Voltage
.VOLTAGE_400
650 logger
.error(`${logPrefix} ${errorMsg}`)
651 throw new BaseError(errorMsg
)
653 return defaultVoltageOut
656 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
657 return stationInfo
.idTagsFile
!= null
658 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
662 export const waitChargingStationEvents
= async (
663 emitter
: EventEmitter
,
664 event
: ChargingStationWorkerMessageEvents
,
666 ): Promise
<number> => {
667 return await new Promise
<number>((resolve
) => {
669 if (eventsToWait
=== 0) {
673 emitter
.on(event
, () => {
675 if (events
=== eventsToWait
) {
682 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
683 let configuredMaxNumberOfConnectors
= 0
684 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
685 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[]
686 configuredMaxNumberOfConnectors
=
687 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
688 } else if (!isUndefined(stationTemplate
.numberOfConnectors
)) {
689 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number
690 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
691 configuredMaxNumberOfConnectors
=
692 stationTemplate
.Connectors
?.[0] != null
693 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
694 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
695 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
696 for (const evse
in stationTemplate
.Evses
) {
700 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
701 stationTemplate
.Evses
[evse
].Connectors
705 return configuredMaxNumberOfConnectors
708 const checkConfiguredMaxConnectors
= (
709 configuredMaxConnectors
: number,
713 if (configuredMaxConnectors
<= 0) {
715 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
720 const checkTemplateMaxConnectors
= (
721 templateMaxConnectors
: number,
725 if (templateMaxConnectors
=== 0) {
727 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
729 } else if (templateMaxConnectors
< 0) {
731 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
736 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
737 connectorStatus
.availability
= AvailabilityType
.Operative
738 connectorStatus
.idTagLocalAuthorized
= false
739 connectorStatus
.idTagAuthorized
= false
740 connectorStatus
.transactionRemoteStarted
= false
741 connectorStatus
.transactionStarted
= false
742 connectorStatus
.energyActiveImportRegisterValue
= 0
743 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
744 if (isUndefined(connectorStatus
.chargingProfiles
)) {
745 connectorStatus
.chargingProfiles
= []
749 const warnDeprecatedTemplateKey
= (
750 template
: ChargingStationTemplate
,
753 templateFile
: string,
756 if (!isUndefined(template
?.[key
as keyof ChargingStationTemplate
])) {
757 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
758 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
760 logger
.warn(`${logPrefix} ${logMsg}`)
761 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
765 const convertDeprecatedTemplateKey
= (
766 template
: ChargingStationTemplate
,
767 deprecatedKey
: string,
770 if (!isUndefined(template
?.[deprecatedKey
as keyof ChargingStationTemplate
])) {
771 if (!isUndefined(key
)) {
772 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
773 (template
as unknown
as Record
<string, unknown
>)[key
!] =
774 template
[deprecatedKey
as keyof ChargingStationTemplate
]
776 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
777 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
781 interface ChargingProfilesLimit
{
783 chargingProfile
: ChargingProfile
787 * Charging profiles shall already be sorted by connector id descending then stack level descending
789 * @param chargingStation -
790 * @param connectorId -
791 * @param chargingProfiles -
793 * @returns ChargingProfilesLimit
795 const getLimitFromChargingProfiles
= (
796 chargingStation
: ChargingStation
,
798 chargingProfiles
: ChargingProfile
[],
800 ): ChargingProfilesLimit
| undefined => {
801 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
802 const currentDate
= new Date()
803 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
804 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!
805 for (const chargingProfile
of chargingProfiles
) {
806 const chargingSchedule
= chargingProfile
.chargingSchedule
808 isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
809 connectorStatus
?.transactionStarted
=== true
812 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
814 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
815 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
818 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
819 !isDate(chargingSchedule
?.startSchedule
)
822 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
824 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
825 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!
828 !isNullOrUndefined(chargingSchedule
?.startSchedule
) &&
829 isNullOrUndefined(chargingSchedule
?.duration
)
832 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
834 // OCPP specifies that if duration is not defined, it should be infinite
835 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
836 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
!)
838 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
841 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
844 // Check if the charging profile is active
846 isWithinInterval(currentDate
, {
847 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
848 start
: chargingSchedule
.startSchedule
!,
849 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
850 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!)
853 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
854 const chargingSchedulePeriodCompareFn
= (
855 a
: ChargingSchedulePeriod
,
856 b
: ChargingSchedulePeriod
857 ): number => a
.startPeriod
- b
.startPeriod
859 !isArraySorted
<ChargingSchedulePeriod
>(
860 chargingSchedule
.chargingSchedulePeriod
,
861 chargingSchedulePeriodCompareFn
865 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
867 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
869 // Check if the first schedule period startPeriod property is equal to 0
870 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
872 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
876 // Handle only one schedule period
877 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
878 const result
: ChargingProfilesLimit
= {
879 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
882 logger
.debug(debugLogMsg
, result
)
885 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
886 // Search for the right schedule period
889 chargingSchedulePeriod
890 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
891 // Find the right schedule period
894 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
895 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
899 // Found the schedule period: previous is the correct one
900 const result
: ChargingProfilesLimit
= {
901 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
902 limit
: previousChargingSchedulePeriod
!.limit
,
905 logger
.debug(debugLogMsg
, result
)
908 // Keep a reference to previous one
909 previousChargingSchedulePeriod
= chargingSchedulePeriod
910 // Handle the last schedule period within the charging profile duration
912 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
913 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
916 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
917 chargingSchedule
.startSchedule
!,
918 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
920 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
921 chargingSchedule
.startSchedule
!
922 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
923 ) > chargingSchedule
.duration
!)
925 const result
: ChargingProfilesLimit
= {
926 limit
: previousChargingSchedulePeriod
.limit
,
929 logger
.debug(debugLogMsg
, result
)
938 export const prepareChargingProfileKind
= (
939 connectorStatus
: ConnectorStatus
,
940 chargingProfile
: ChargingProfile
,
944 switch (chargingProfile
.chargingProfileKind
) {
945 case ChargingProfileKindType
.RECURRING
:
946 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
949 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
951 case ChargingProfileKindType
.RELATIVE
:
952 if (!isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)) {
954 `${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`
956 delete chargingProfile
.chargingSchedule
.startSchedule
958 if (connectorStatus
?.transactionStarted
=== true) {
959 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
961 // FIXME: Handle relative charging profile duration
967 export const canProceedChargingProfile
= (
968 chargingProfile
: ChargingProfile
,
973 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
974 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
975 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
976 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
979 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
980 chargingProfile.chargingProfileId
981 } is not valid for the current date ${currentDate.toISOString()}`
986 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) ||
987 isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)
990 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
995 !isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
) &&
996 !isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)
999 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1004 !isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
) &&
1005 !Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)
1008 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1015 const canProceedRecurringChargingProfile
= (
1016 chargingProfile
: ChargingProfile
,
1020 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1021 isNullOrUndefined(chargingProfile
.recurrencyKind
)
1024 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1029 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1030 isNullOrUndefined(chargingProfile
.chargingSchedule
.startSchedule
)
1033 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1041 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1043 * @param chargingProfile -
1044 * @param currentDate -
1045 * @param logPrefix -
1047 const prepareRecurringChargingProfile
= (
1048 chargingProfile
: ChargingProfile
,
1052 const chargingSchedule
= chargingProfile
.chargingSchedule
1053 let recurringIntervalTranslated
= false
1054 let recurringInterval
: Interval
1055 switch (chargingProfile
.recurrencyKind
) {
1056 case RecurrencyKindType
.DAILY
:
1057 recurringInterval
= {
1058 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1059 start
: chargingSchedule
.startSchedule
!,
1060 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1061 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1063 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1065 !isWithinInterval(currentDate
, recurringInterval
) &&
1066 isBefore(recurringInterval
.end
, currentDate
)
1068 chargingSchedule
.startSchedule
= addDays(
1069 recurringInterval
.start
,
1070 differenceInDays(currentDate
, recurringInterval
.start
)
1072 recurringInterval
= {
1073 start
: chargingSchedule
.startSchedule
,
1074 end
: addDays(chargingSchedule
.startSchedule
, 1)
1076 recurringIntervalTranslated
= true
1079 case RecurrencyKindType
.WEEKLY
:
1080 recurringInterval
= {
1081 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1082 start
: chargingSchedule
.startSchedule
!,
1083 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1084 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1086 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1088 !isWithinInterval(currentDate
, recurringInterval
) &&
1089 isBefore(recurringInterval
.end
, currentDate
)
1091 chargingSchedule
.startSchedule
= addWeeks(
1092 recurringInterval
.start
,
1093 differenceInWeeks(currentDate
, recurringInterval
.start
)
1095 recurringInterval
= {
1096 start
: chargingSchedule
.startSchedule
,
1097 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1099 recurringIntervalTranslated
= true
1104 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1107 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1108 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1110 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1111 chargingProfile.recurrencyKind
1112 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1113 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1114 recurringInterval!.start
1115 ).toISOString()}, ${toDate(
1116 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1117 recurringInterval!.end
1118 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
1121 return recurringIntervalTranslated
1124 const checkRecurringChargingProfileDuration
= (
1125 chargingProfile
: ChargingProfile
,
1129 if (isNullOrUndefined(chargingProfile
.chargingSchedule
.duration
)) {
1131 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1132 chargingProfile.chargingProfileKind
1133 } charging profile id ${
1134 chargingProfile.chargingProfileId
1135 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1140 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1142 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1143 chargingProfile
.chargingSchedule
.duration
! > differenceInSeconds(interval
.end
, interval
.start
)
1146 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1147 chargingProfile.chargingProfileKind
1148 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1149 chargingProfile.chargingSchedule.duration
1150 } is greater than the recurrency time interval duration ${differenceInSeconds(
1155 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1159 const getRandomSerialNumberSuffix
= (params
?: {
1160 randomBytesLength
?: number
1163 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1164 if (params
?.upperCase
=== true) {
1165 return randomSerialNumberSuffix
.toUpperCase()
1167 return randomSerialNumberSuffix