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'
71 } from
'../utils/index.js'
73 const moduleName
= 'Helpers'
75 export const getChargingStationId
= (
77 stationTemplate
: ChargingStationTemplate
| undefined
79 if (stationTemplate
== null) {
80 return "Unknown 'chargingStationId'"
82 // In case of multiple instances: add instance index to charging station id
83 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
84 const idSuffix
= stationTemplate
.nameSuffix
?? ''
85 const idStr
= `000000000${index.toString()}`
86 return stationTemplate
.fixedName
=== true
87 ? stationTemplate
.baseName
88 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
93 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
94 return isPast(reservation
.expiryDate
)
97 export const removeExpiredReservations
= async (
98 chargingStation
: ChargingStation
100 if (chargingStation
.hasEvses
) {
101 for (const evseStatus
of chargingStation
.evses
.values()) {
102 for (const connectorStatus
of evseStatus
.connectors
.values()) {
104 connectorStatus
.reservation
!= null &&
105 hasReservationExpired(connectorStatus
.reservation
)
107 await chargingStation
.removeReservation(
108 connectorStatus
.reservation
,
109 ReservationTerminationReason
.EXPIRED
115 for (const connectorStatus
of chargingStation
.connectors
.values()) {
117 connectorStatus
.reservation
!= null &&
118 hasReservationExpired(connectorStatus
.reservation
)
120 await chargingStation
.removeReservation(
121 connectorStatus
.reservation
,
122 ReservationTerminationReason
.EXPIRED
129 export const getNumberOfReservableConnectors
= (
130 connectors
: Map
<number, ConnectorStatus
>
132 let numberOfReservableConnectors
= 0
133 for (const [connectorId
, connectorStatus
] of connectors
) {
134 if (connectorId
=== 0) {
137 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
138 ++numberOfReservableConnectors
141 return numberOfReservableConnectors
144 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
145 const chargingStationInfo
= {
146 chargePointModel
: stationTemplate
.chargePointModel
,
147 chargePointVendor
: stationTemplate
.chargePointVendor
,
148 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!== undefined && {
149 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
151 ...(stationTemplate
.chargePointSerialNumberPrefix
!== undefined && {
152 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
154 ...(stationTemplate
.meterSerialNumberPrefix
!== undefined && {
155 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
157 ...(stationTemplate
.meterType
!== undefined && {
158 meterType
: stationTemplate
.meterType
161 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
162 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
166 export const checkChargingStation
= (
167 chargingStation
: ChargingStation
,
170 if (!chargingStation
.started
&& !chargingStation
.starting
) {
171 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
177 export const getPhaseRotationValue
= (
179 numberOfPhases
: number
180 ): string | undefined => {
182 if (connectorId
=== 0 && numberOfPhases
=== 0) {
183 return `${connectorId}.${ConnectorPhaseRotation.RST}`
184 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
185 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
187 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
188 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
189 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
190 return `${connectorId}.${ConnectorPhaseRotation.RST}`
194 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
198 return Object.keys(evses
).length
201 const getMaxNumberOfConnectors
= (
202 connectors
: Record
<string, ConnectorStatus
> | undefined
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
| undefined,
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 if (stationTemplate
.idTagsFile
== null || isEmptyString(stationTemplate
.idTagsFile
)) {
252 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
257 export const checkConfiguration
= (
258 stationConfiguration
: ChargingStationConfiguration
| undefined,
260 configurationFile
: string
262 if (stationConfiguration
== null) {
263 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
264 logger
.error(`${logPrefix} ${errorMsg}`)
265 throw new BaseError(errorMsg
)
267 if (isEmptyObject(stationConfiguration
)) {
268 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
269 logger
.error(`${logPrefix} ${errorMsg}`)
270 throw new BaseError(errorMsg
)
274 export const checkConnectorsConfiguration
= (
275 stationTemplate
: ChargingStationTemplate
,
279 configuredMaxConnectors
: number
280 templateMaxConnectors
: number
281 templateMaxAvailableConnectors
: number
283 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
284 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
285 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
286 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
287 const templateMaxAvailableConnectors
=
288 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
290 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
291 stationTemplate
.randomConnectors
!== true
294 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
296 stationTemplate
.randomConnectors
= true
298 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
}
301 export const checkStationInfoConnectorStatus
= (
303 connectorStatus
: ConnectorStatus
,
307 if (connectorStatus
.status != null) {
309 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
311 delete connectorStatus
.status
315 export const buildConnectorsMap
= (
316 connectors
: Record
<string, ConnectorStatus
>,
319 ): Map
<number, ConnectorStatus
> => {
320 const connectorsMap
= new Map
<number, ConnectorStatus
>()
321 if (getMaxNumberOfConnectors(connectors
) > 0) {
322 for (const connector
in connectors
) {
323 const connectorStatus
= connectors
[connector
]
324 const connectorId
= convertToInt(connector
)
325 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
326 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
330 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
336 export const initializeConnectorsMapStatus
= (
337 connectors
: Map
<number, ConnectorStatus
>,
340 for (const connectorId
of connectors
.keys()) {
341 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
343 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
344 connectors.get(connectorId)?.transactionId
348 if (connectorId
=== 0) {
349 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
350 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
351 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
352 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353 connectors
.get(connectorId
)!.chargingProfiles
= []
355 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
356 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
357 initializeConnectorStatus(connectors
.get(connectorId
)!)
362 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
363 if (connectorStatus
== null) {
366 connectorStatus
.chargingProfiles
=
367 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
368 ? connectorStatus
.chargingProfiles
.filter(
369 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
372 connectorStatus
.idTagLocalAuthorized
= false
373 connectorStatus
.idTagAuthorized
= false
374 connectorStatus
.transactionRemoteStarted
= false
375 connectorStatus
.transactionStarted
= false
376 delete connectorStatus
.transactionStart
377 delete connectorStatus
.transactionId
378 delete connectorStatus
.localAuthorizeIdTag
379 delete connectorStatus
.authorizeIdTag
380 delete connectorStatus
.transactionIdTag
381 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
382 delete connectorStatus
.transactionBeginMeterValue
385 export const createBootNotificationRequest
= (
386 stationInfo
: ChargingStationInfo
,
387 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
388 ): BootNotificationRequest
| undefined => {
389 const ocppVersion
= stationInfo
.ocppVersion
390 switch (ocppVersion
) {
391 case OCPPVersion
.VERSION_16
:
393 chargePointModel
: stationInfo
.chargePointModel
,
394 chargePointVendor
: stationInfo
.chargePointVendor
,
395 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
396 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
398 ...(stationInfo
.chargePointSerialNumber
!== undefined && {
399 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
401 ...(stationInfo
.firmwareVersion
!== undefined && {
402 firmwareVersion
: stationInfo
.firmwareVersion
404 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
405 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
}),
406 ...(stationInfo
.meterSerialNumber
!== undefined && {
407 meterSerialNumber
: stationInfo
.meterSerialNumber
409 ...(stationInfo
.meterType
!== undefined && {
410 meterType
: stationInfo
.meterType
412 } satisfies OCPP16BootNotificationRequest
413 case OCPPVersion
.VERSION_20
:
414 case OCPPVersion
.VERSION_201
:
418 model
: stationInfo
.chargePointModel
,
419 vendorName
: stationInfo
.chargePointVendor
,
420 ...(stationInfo
.firmwareVersion
!== undefined && {
421 firmwareVersion
: stationInfo
.firmwareVersion
423 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
424 serialNumber
: stationInfo
.chargeBoxSerialNumber
426 ...((stationInfo
.iccid
!== undefined || stationInfo
.imsi
!== undefined) && {
428 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
429 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
})
433 } satisfies OCPP20BootNotificationRequest
437 export const warnTemplateKeysDeprecation
= (
438 stationTemplate
: ChargingStationTemplate
,
442 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
443 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
444 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
445 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
446 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
448 for (const templateKey
of templateKeys
) {
449 warnDeprecatedTemplateKey(
451 templateKey
.deprecatedKey
,
454 templateKey
.key
!== undefined ? `Use '${templateKey.key}' instead` : undefined
456 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
460 export const stationTemplateToStationInfo
= (
461 stationTemplate
: ChargingStationTemplate
462 ): ChargingStationInfo
=> {
463 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
464 delete stationTemplate
.power
465 delete stationTemplate
.powerUnit
466 delete stationTemplate
.Connectors
467 delete stationTemplate
.Evses
468 delete stationTemplate
.Configuration
469 delete stationTemplate
.AutomaticTransactionGenerator
470 delete stationTemplate
.chargeBoxSerialNumberPrefix
471 delete stationTemplate
.chargePointSerialNumberPrefix
472 delete stationTemplate
.meterSerialNumberPrefix
473 return stationTemplate
as ChargingStationInfo
476 export const createSerialNumber
= (
477 stationTemplate
: ChargingStationTemplate
,
478 stationInfo
: ChargingStationInfo
,
480 randomSerialNumberUpperCase
?: boolean
481 randomSerialNumber
?: boolean
484 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
485 const serialNumberSuffix
=
486 params
.randomSerialNumber
=== true
487 ? getRandomSerialNumberSuffix({
488 upperCase
: params
.randomSerialNumberUpperCase
491 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
492 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
493 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
494 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
495 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
496 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
499 export const propagateSerialNumber
= (
500 stationTemplate
: ChargingStationTemplate
| undefined,
501 stationInfoSrc
: ChargingStationInfo
| undefined,
502 stationInfoDst
: ChargingStationInfo
504 if (stationInfoSrc
== null || stationTemplate
== null) {
506 'Missing charging station template or existing configuration to propagate serial number'
509 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
510 stationInfoSrc
.chargePointSerialNumber
!= null
511 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
512 : stationInfoDst
.chargePointSerialNumber
!= null &&
513 delete stationInfoDst
.chargePointSerialNumber
514 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
515 stationInfoSrc
.chargeBoxSerialNumber
!= null
516 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
517 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
518 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
519 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
520 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
523 export const hasFeatureProfile
= (
524 chargingStation
: ChargingStation
,
525 featureProfile
: SupportedFeatureProfiles
526 ): boolean | undefined => {
527 return getConfigurationKey(
529 StandardParametersKey
.SupportedFeatureProfiles
530 )?.value
?.includes(featureProfile
)
533 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
535 switch (stationInfo
.amperageLimitationUnit
) {
536 case AmpereUnits
.DECI_AMPERE
:
539 case AmpereUnits
.CENTI_AMPERE
:
542 case AmpereUnits
.MILLI_AMPERE
:
550 * Gets the connector cloned charging profiles applying a power limitation
551 * and sorted by connector id descending then stack level descending
553 * @param chargingStation -
554 * @param connectorId -
555 * @returns connector charging profiles array
557 export const getConnectorChargingProfiles
= (
558 chargingStation
: ChargingStation
,
560 ): ChargingProfile
[] => {
561 return clone
<ChargingProfile
[]>(
562 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
563 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
565 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
566 (a
, b
) => b
.stackLevel
- a
.stackLevel
572 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
573 chargingStation
: ChargingStation
,
575 ): number | undefined => {
576 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
577 // Get charging profiles sorted by connector id then stack level
578 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
579 if (isNotEmptyArray(chargingProfiles
)) {
580 const result
= getLimitFromChargingProfiles(
584 chargingStation
.logPrefix()
586 if (result
!= null) {
588 chargingProfile
= result
.chargingProfile
589 switch (chargingStation
.stationInfo
?.currentOutType
) {
592 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
594 : ACElectricUtils
.powerTotal(
595 chargingStation
.getNumberOfPhases(),
596 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
597 chargingStation
.stationInfo
.voltageOut
!,
603 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
605 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
606 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
608 const connectorMaximumPower
=
609 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
610 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
611 if (limit
> connectorMaximumPower
) {
613 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
614 chargingProfile.chargingProfileId
615 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
618 limit
= connectorMaximumPower
625 export const getDefaultVoltageOut
= (
626 currentType
: CurrentType
,
630 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
631 let defaultVoltageOut
: number
632 switch (currentType
) {
634 defaultVoltageOut
= Voltage
.VOLTAGE_230
637 defaultVoltageOut
= Voltage
.VOLTAGE_400
640 logger
.error(`${logPrefix} ${errorMsg}`)
641 throw new BaseError(errorMsg
)
643 return defaultVoltageOut
646 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
647 return stationInfo
.idTagsFile
!= null
648 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
652 export const waitChargingStationEvents
= async (
653 emitter
: EventEmitter
,
654 event
: ChargingStationWorkerMessageEvents
,
656 ): Promise
<number> => {
657 return await new Promise
<number>(resolve
=> {
659 if (eventsToWait
=== 0) {
663 emitter
.on(event
, () => {
665 if (events
=== eventsToWait
) {
672 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
673 let configuredMaxNumberOfConnectors
= 0
674 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
675 const numberOfConnectors
= stationTemplate
.numberOfConnectors
676 configuredMaxNumberOfConnectors
=
677 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
678 } else if (stationTemplate
.numberOfConnectors
!= null) {
679 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
680 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
681 configuredMaxNumberOfConnectors
=
682 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
683 stationTemplate
.Connectors
[0] != null
684 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
685 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
686 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
687 for (const evse
in stationTemplate
.Evses
) {
691 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
692 stationTemplate
.Evses
[evse
].Connectors
696 return configuredMaxNumberOfConnectors
699 const checkConfiguredMaxConnectors
= (
700 configuredMaxConnectors
: number,
704 if (configuredMaxConnectors
<= 0) {
706 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
711 const checkTemplateMaxConnectors
= (
712 templateMaxConnectors
: number,
716 if (templateMaxConnectors
=== 0) {
718 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
720 } else if (templateMaxConnectors
< 0) {
722 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
727 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
728 connectorStatus
.availability
= AvailabilityType
.Operative
729 connectorStatus
.idTagLocalAuthorized
= false
730 connectorStatus
.idTagAuthorized
= false
731 connectorStatus
.transactionRemoteStarted
= false
732 connectorStatus
.transactionStarted
= false
733 connectorStatus
.energyActiveImportRegisterValue
= 0
734 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
735 if (connectorStatus
.chargingProfiles
== null) {
736 connectorStatus
.chargingProfiles
= []
740 const warnDeprecatedTemplateKey
= (
741 template
: ChargingStationTemplate
,
744 templateFile
: string,
747 if (template
[key
as keyof ChargingStationTemplate
] !== undefined) {
748 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
749 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
751 logger
.warn(`${logPrefix} ${logMsg}`)
752 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
756 const convertDeprecatedTemplateKey
= (
757 template
: ChargingStationTemplate
,
758 deprecatedKey
: string,
761 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] !== undefined) {
762 if (key
!== undefined) {
763 (template
as unknown
as Record
<string, unknown
>)[key
] =
764 template
[deprecatedKey
as keyof ChargingStationTemplate
]
766 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
767 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
771 interface ChargingProfilesLimit
{
773 chargingProfile
: ChargingProfile
777 * Charging profiles shall already be sorted by connector id descending then stack level descending
779 * @param chargingStation -
780 * @param connectorId -
781 * @param chargingProfiles -
783 * @returns ChargingProfilesLimit
785 const getLimitFromChargingProfiles
= (
786 chargingStation
: ChargingStation
,
788 chargingProfiles
: ChargingProfile
[],
790 ): ChargingProfilesLimit
| undefined => {
791 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
792 const currentDate
= new Date()
793 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
794 for (const chargingProfile
of chargingProfiles
) {
795 const chargingSchedule
= chargingProfile
.chargingSchedule
796 if (chargingSchedule
.startSchedule
== null) {
798 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
800 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
801 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
803 if (!isDate(chargingSchedule
.startSchedule
)) {
805 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
807 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
808 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
810 if (chargingSchedule
.duration
== null) {
812 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
814 // OCPP specifies that if duration is not defined, it should be infinite
815 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
817 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
820 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
823 // Check if the charging profile is active
825 isWithinInterval(currentDate
, {
826 start
: chargingSchedule
.startSchedule
,
827 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
830 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
831 const chargingSchedulePeriodCompareFn
= (
832 a
: ChargingSchedulePeriod
,
833 b
: ChargingSchedulePeriod
834 ): number => a
.startPeriod
- b
.startPeriod
836 !isArraySorted
<ChargingSchedulePeriod
>(
837 chargingSchedule
.chargingSchedulePeriod
,
838 chargingSchedulePeriodCompareFn
842 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
844 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
846 // Check if the first schedule period startPeriod property is equal to 0
847 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
849 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
853 // Handle only one schedule period
854 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
855 const result
: ChargingProfilesLimit
= {
856 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
859 logger
.debug(debugLogMsg
, result
)
862 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
863 // Search for the right schedule period
866 chargingSchedulePeriod
867 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
868 // Find the right schedule period
871 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
875 // Found the schedule period: previous is the correct one
876 const result
: ChargingProfilesLimit
= {
877 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
878 limit
: previousChargingSchedulePeriod
!.limit
,
881 logger
.debug(debugLogMsg
, result
)
884 // Keep a reference to previous one
885 previousChargingSchedulePeriod
= chargingSchedulePeriod
886 // Handle the last schedule period within the charging profile duration
888 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
889 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
892 chargingSchedule
.startSchedule
,
893 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
895 chargingSchedule
.startSchedule
896 ) > chargingSchedule
.duration
)
898 const result
: ChargingProfilesLimit
= {
899 limit
: previousChargingSchedulePeriod
.limit
,
902 logger
.debug(debugLogMsg
, result
)
911 export const prepareChargingProfileKind
= (
912 connectorStatus
: ConnectorStatus
| undefined,
913 chargingProfile
: ChargingProfile
,
914 currentDate
: string | number | Date,
917 switch (chargingProfile
.chargingProfileKind
) {
918 case ChargingProfileKindType
.RECURRING
:
919 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
922 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
924 case ChargingProfileKindType
.RELATIVE
:
925 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
927 `${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`
929 delete chargingProfile
.chargingSchedule
.startSchedule
931 if (connectorStatus
?.transactionStarted
=== true) {
932 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
934 // FIXME: Handle relative charging profile duration
940 export const canProceedChargingProfile
= (
941 chargingProfile
: ChargingProfile
,
942 currentDate
: string | number | Date,
946 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
947 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
950 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
951 chargingProfile.chargingProfileId
952 } is not valid for the current date ${
953 isDate(currentDate) ? currentDate.toISOString() : currentDate
959 chargingProfile
.chargingSchedule
.startSchedule
== null ||
960 chargingProfile
.chargingSchedule
.duration
== null
963 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
967 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
969 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
973 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
975 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
982 const canProceedRecurringChargingProfile
= (
983 chargingProfile
: ChargingProfile
,
987 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
988 chargingProfile
.recurrencyKind
== null
991 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
996 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
997 chargingProfile
.chargingSchedule
.startSchedule
== null
1000 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1008 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1010 * @param chargingProfile -
1011 * @param currentDate -
1012 * @param logPrefix -
1014 const prepareRecurringChargingProfile
= (
1015 chargingProfile
: ChargingProfile
,
1016 currentDate
: string | number | Date,
1019 const chargingSchedule
= chargingProfile
.chargingSchedule
1020 let recurringIntervalTranslated
= false
1021 let recurringInterval
: Interval
| undefined
1022 switch (chargingProfile
.recurrencyKind
) {
1023 case RecurrencyKindType
.DAILY
:
1024 recurringInterval
= {
1025 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1026 start
: chargingSchedule
.startSchedule
!,
1027 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1028 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1030 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1032 !isWithinInterval(currentDate
, recurringInterval
) &&
1033 isBefore(recurringInterval
.end
, currentDate
)
1035 chargingSchedule
.startSchedule
= addDays(
1036 recurringInterval
.start
,
1037 differenceInDays(currentDate
, recurringInterval
.start
)
1039 recurringInterval
= {
1040 start
: chargingSchedule
.startSchedule
,
1041 end
: addDays(chargingSchedule
.startSchedule
, 1)
1043 recurringIntervalTranslated
= true
1046 case RecurrencyKindType
.WEEKLY
:
1047 recurringInterval
= {
1048 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1049 start
: chargingSchedule
.startSchedule
!,
1050 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1051 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1053 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1055 !isWithinInterval(currentDate
, recurringInterval
) &&
1056 isBefore(recurringInterval
.end
, currentDate
)
1058 chargingSchedule
.startSchedule
= addWeeks(
1059 recurringInterval
.start
,
1060 differenceInWeeks(currentDate
, recurringInterval
.start
)
1062 recurringInterval
= {
1063 start
: chargingSchedule
.startSchedule
,
1064 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1066 recurringIntervalTranslated
= true
1071 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1074 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1075 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1077 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1078 chargingProfile.recurrencyKind
1079 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1080 recurringInterval?.start as Date
1081 ).toISOString()}, ${toDate(
1082 recurringInterval?.end as Date
1083 ).toISOString()}] has not been properly translated to current date ${
1084 isDate(currentDate) ? currentDate.toISOString() : currentDate
1088 return recurringIntervalTranslated
1091 const checkRecurringChargingProfileDuration
= (
1092 chargingProfile
: ChargingProfile
,
1096 if (chargingProfile
.chargingSchedule
.duration
== null) {
1098 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1099 chargingProfile.chargingProfileKind
1100 } charging profile id ${
1101 chargingProfile.chargingProfileId
1102 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1107 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1109 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1112 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1113 chargingProfile.chargingProfileKind
1114 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1115 chargingProfile.chargingSchedule.duration
1116 } is greater than the recurrency time interval duration ${differenceInSeconds(
1121 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1125 const getRandomSerialNumberSuffix
= (params
?: {
1126 randomBytesLength
?: number
1129 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1130 if (params
?.upperCase
=== true) {
1131 return randomSerialNumberSuffix
.toUpperCase()
1133 return randomSerialNumberSuffix