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
>): number => {
198 return Object.keys(evses
).length
201 const getMaxNumberOfConnectors
= (connectors
: Record
<string, ConnectorStatus
>): number => {
202 if (connectors
== null) {
205 return Object.keys(connectors
).length
208 export const getBootConnectorStatus
= (
209 chargingStation
: ChargingStation
,
211 connectorStatus
: ConnectorStatus
212 ): ConnectorStatusEnum
=> {
213 let connectorBootStatus
: ConnectorStatusEnum
215 connectorStatus
?.status == null &&
216 (!chargingStation
.isChargingStationAvailable() ||
217 !chargingStation
.isConnectorAvailable(connectorId
))
219 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
220 } else if (connectorStatus
?.status == null && connectorStatus
?.bootStatus
!= null) {
221 // Set boot status in template at startup
222 connectorBootStatus
= connectorStatus
?.bootStatus
223 } else if (connectorStatus
?.status != null) {
224 // Set previous status at startup
225 connectorBootStatus
= connectorStatus
?.status
227 // Set default status
228 connectorBootStatus
= ConnectorStatusEnum
.Available
230 return connectorBootStatus
233 export const checkTemplate
= (
234 stationTemplate
: ChargingStationTemplate
,
238 if (stationTemplate
== null) {
239 const errorMsg
= `Failed to read charging station template file ${templateFile}`
240 logger
.error(`${logPrefix} ${errorMsg}`)
241 throw new BaseError(errorMsg
)
243 if (isEmptyObject(stationTemplate
)) {
244 const errorMsg
= `Empty charging station information from template file ${templateFile}`
245 logger
.error(`${logPrefix} ${errorMsg}`)
246 throw new BaseError(errorMsg
)
248 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
249 if (isEmptyObject(stationTemplate
.AutomaticTransactionGenerator
!)) {
250 stationTemplate
.AutomaticTransactionGenerator
= Constants
.DEFAULT_ATG_CONFIGURATION
252 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
253 Constants
.DEFAULT_ATG_CONFIGURATION
256 if (stationTemplate
.idTagsFile
== null || isEmptyString(stationTemplate
.idTagsFile
)) {
258 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
263 export const checkConfiguration
= (
264 stationConfiguration
: ChargingStationConfiguration
| undefined,
266 configurationFile
: string
268 if (stationConfiguration
== null) {
269 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
270 logger
.error(`${logPrefix} ${errorMsg}`)
271 throw new BaseError(errorMsg
)
273 if (isEmptyObject(stationConfiguration
)) {
274 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
275 logger
.error(`${logPrefix} ${errorMsg}`)
276 throw new BaseError(errorMsg
)
280 export const checkConnectorsConfiguration
= (
281 stationTemplate
: ChargingStationTemplate
,
285 configuredMaxConnectors
: number
286 templateMaxConnectors
: number
287 templateMaxAvailableConnectors
: number
289 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
290 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
291 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
292 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
!)
293 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
294 const templateMaxAvailableConnectors
=
295 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
297 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
298 stationTemplate
?.randomConnectors
=== false
301 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
303 stationTemplate
.randomConnectors
= true
305 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
}
308 export const checkStationInfoConnectorStatus
= (
310 connectorStatus
: ConnectorStatus
,
314 if (connectorStatus
?.status != null) {
316 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
318 delete connectorStatus
.status
322 export const buildConnectorsMap
= (
323 connectors
: Record
<string, ConnectorStatus
>,
326 ): Map
<number, ConnectorStatus
> => {
327 const connectorsMap
= new Map
<number, ConnectorStatus
>()
328 if (getMaxNumberOfConnectors(connectors
) > 0) {
329 for (const connector
in connectors
) {
330 const connectorStatus
= connectors
[connector
]
331 const connectorId
= convertToInt(connector
)
332 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
333 connectorsMap
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
))
337 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
343 export const initializeConnectorsMapStatus
= (
344 connectors
: Map
<number, ConnectorStatus
>,
347 for (const connectorId
of connectors
.keys()) {
348 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
350 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
355 if (connectorId
=== 0) {
356 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
357 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
358 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
359 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
360 connectors
.get(connectorId
)!.chargingProfiles
= []
362 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
363 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
364 initializeConnectorStatus(connectors
.get(connectorId
)!)
369 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
370 connectorStatus
.chargingProfiles
=
371 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
372 ? connectorStatus
.chargingProfiles
?.filter(
373 (chargingProfile
) => chargingProfile
.transactionId
!== connectorStatus
.transactionId
376 connectorStatus
.idTagLocalAuthorized
= false
377 connectorStatus
.idTagAuthorized
= false
378 connectorStatus
.transactionRemoteStarted
= false
379 connectorStatus
.transactionStarted
= false
380 delete connectorStatus
?.transactionStart
381 delete connectorStatus
?.transactionId
382 delete connectorStatus
?.localAuthorizeIdTag
383 delete connectorStatus
?.authorizeIdTag
384 delete connectorStatus
?.transactionIdTag
385 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
386 delete connectorStatus
?.transactionBeginMeterValue
389 export const createBootNotificationRequest
= (
390 stationInfo
: ChargingStationInfo
,
391 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
392 ): BootNotificationRequest
=> {
393 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
394 const ocppVersion
= stationInfo
.ocppVersion
!
395 switch (ocppVersion
) {
396 case OCPPVersion
.VERSION_16
:
398 chargePointModel
: stationInfo
.chargePointModel
,
399 chargePointVendor
: stationInfo
.chargePointVendor
,
400 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
401 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
403 ...(stationInfo
.chargePointSerialNumber
!== undefined && {
404 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
406 ...(stationInfo
.firmwareVersion
!== undefined && {
407 firmwareVersion
: stationInfo
.firmwareVersion
409 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
410 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
}),
411 ...(stationInfo
.meterSerialNumber
!== undefined && {
412 meterSerialNumber
: stationInfo
.meterSerialNumber
414 ...(stationInfo
.meterType
!== undefined && {
415 meterType
: stationInfo
.meterType
417 } satisfies OCPP16BootNotificationRequest
418 case OCPPVersion
.VERSION_20
:
419 case OCPPVersion
.VERSION_201
:
423 model
: stationInfo
.chargePointModel
,
424 vendorName
: stationInfo
.chargePointVendor
,
425 ...(stationInfo
.firmwareVersion
!== undefined && {
426 firmwareVersion
: stationInfo
.firmwareVersion
428 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
429 serialNumber
: stationInfo
.chargeBoxSerialNumber
431 ...((stationInfo
.iccid
!== undefined || stationInfo
.imsi
!== undefined) && {
433 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
434 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
})
438 } satisfies OCPP20BootNotificationRequest
442 export const warnTemplateKeysDeprecation
= (
443 stationTemplate
: ChargingStationTemplate
,
447 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
448 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
449 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
450 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
451 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
453 for (const templateKey
of templateKeys
) {
454 warnDeprecatedTemplateKey(
456 templateKey
.deprecatedKey
,
459 templateKey
.key
!== undefined ? `Use '${templateKey.key}' instead` : undefined
461 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
465 export const stationTemplateToStationInfo
= (
466 stationTemplate
: ChargingStationTemplate
467 ): ChargingStationInfo
=> {
468 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
)
469 delete stationTemplate
.power
470 delete stationTemplate
.powerUnit
471 delete stationTemplate
.Connectors
472 delete stationTemplate
.Evses
473 delete stationTemplate
.Configuration
474 delete stationTemplate
.AutomaticTransactionGenerator
475 delete stationTemplate
.chargeBoxSerialNumberPrefix
476 delete stationTemplate
.chargePointSerialNumberPrefix
477 delete stationTemplate
.meterSerialNumberPrefix
478 return stationTemplate
as ChargingStationInfo
481 export const createSerialNumber
= (
482 stationTemplate
: ChargingStationTemplate
,
483 stationInfo
: ChargingStationInfo
,
485 randomSerialNumberUpperCase
?: boolean
486 randomSerialNumber
?: boolean
489 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
490 const serialNumberSuffix
=
491 params
?.randomSerialNumber
=== true
492 ? getRandomSerialNumberSuffix({
493 upperCase
: params
.randomSerialNumberUpperCase
496 isNotEmptyString(stationTemplate
?.chargePointSerialNumberPrefix
) &&
497 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
498 isNotEmptyString(stationTemplate
?.chargeBoxSerialNumberPrefix
) &&
499 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
500 isNotEmptyString(stationTemplate
?.meterSerialNumberPrefix
) &&
501 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
504 export const propagateSerialNumber
= (
505 stationTemplate
: ChargingStationTemplate
,
506 stationInfoSrc
: ChargingStationInfo
,
507 stationInfoDst
: ChargingStationInfo
509 if (stationInfoSrc
== null || stationTemplate
== null) {
511 'Missing charging station template or existing configuration to propagate serial number'
514 stationTemplate
?.chargePointSerialNumberPrefix
!= null &&
515 stationInfoSrc
?.chargePointSerialNumber
!= null
516 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
517 : stationInfoDst
?.chargePointSerialNumber
!= null &&
518 delete stationInfoDst
.chargePointSerialNumber
519 stationTemplate
?.chargeBoxSerialNumberPrefix
!= null &&
520 stationInfoSrc
?.chargeBoxSerialNumber
!= null
521 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
522 : stationInfoDst
?.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
523 stationTemplate
?.meterSerialNumberPrefix
!= null && stationInfoSrc
?.meterSerialNumber
!= null
524 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
525 : stationInfoDst
?.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
528 export const hasFeatureProfile
= (
529 chargingStation
: ChargingStation
,
530 featureProfile
: SupportedFeatureProfiles
531 ): boolean | undefined => {
532 return getConfigurationKey(
534 StandardParametersKey
.SupportedFeatureProfiles
535 )?.value
?.includes(featureProfile
)
538 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
540 switch (stationInfo
.amperageLimitationUnit
) {
541 case AmpereUnits
.DECI_AMPERE
:
544 case AmpereUnits
.CENTI_AMPERE
:
547 case AmpereUnits
.MILLI_AMPERE
:
555 * Gets the connector cloned charging profiles applying a power limitation
556 * and sorted by connector id descending then stack level descending
558 * @param chargingStation -
559 * @param connectorId -
560 * @returns connector charging profiles array
562 export const getConnectorChargingProfiles
= (
563 chargingStation
: ChargingStation
,
565 ): ChargingProfile
[] => {
566 return cloneObject
<ChargingProfile
[]>(
567 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
568 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
570 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
571 (a
, b
) => b
.stackLevel
- a
.stackLevel
577 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
578 chargingStation
: ChargingStation
,
580 ): number | undefined => {
581 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
582 // Get charging profiles sorted by connector id then stack level
583 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
584 if (isNotEmptyArray(chargingProfiles
)) {
585 const result
= getLimitFromChargingProfiles(
589 chargingStation
.logPrefix()
591 if (result
!= null) {
592 limit
= result
?.limit
593 chargingProfile
= result
?.chargingProfile
594 switch (chargingStation
.stationInfo
?.currentOutType
) {
597 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
599 : ACElectricUtils
.powerTotal(
600 chargingStation
.getNumberOfPhases(),
601 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
602 chargingStation
.stationInfo
.voltageOut
!,
608 chargingProfile
?.chargingSchedule
?.chargingRateUnit
=== ChargingRateUnitType
.WATT
610 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
611 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
613 const connectorMaximumPower
=
614 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
615 chargingStation
.stationInfo
.maximumPower
! / chargingStation
.powerDivider
616 if (limit
> connectorMaximumPower
) {
618 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
621 limit
= connectorMaximumPower
628 export const getDefaultVoltageOut
= (
629 currentType
: CurrentType
,
633 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
634 let defaultVoltageOut
: number
635 switch (currentType
) {
637 defaultVoltageOut
= Voltage
.VOLTAGE_230
640 defaultVoltageOut
= Voltage
.VOLTAGE_400
643 logger
.error(`${logPrefix} ${errorMsg}`)
644 throw new BaseError(errorMsg
)
646 return defaultVoltageOut
649 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
650 return stationInfo
.idTagsFile
!= null
651 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
655 export const waitChargingStationEvents
= async (
656 emitter
: EventEmitter
,
657 event
: ChargingStationWorkerMessageEvents
,
659 ): Promise
<number> => {
660 return await new Promise
<number>((resolve
) => {
662 if (eventsToWait
=== 0) {
666 emitter
.on(event
, () => {
668 if (events
=== eventsToWait
) {
675 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
676 let configuredMaxNumberOfConnectors
= 0
677 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
678 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[]
679 configuredMaxNumberOfConnectors
=
680 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
681 } else if (stationTemplate
.numberOfConnectors
!= null) {
682 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number
683 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
684 configuredMaxNumberOfConnectors
=
685 stationTemplate
.Connectors
?.[0] != null
686 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
687 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
688 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
689 for (const evse
in stationTemplate
.Evses
) {
693 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
694 stationTemplate
.Evses
[evse
].Connectors
698 return configuredMaxNumberOfConnectors
701 const checkConfiguredMaxConnectors
= (
702 configuredMaxConnectors
: number,
706 if (configuredMaxConnectors
<= 0) {
708 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
713 const checkTemplateMaxConnectors
= (
714 templateMaxConnectors
: number,
718 if (templateMaxConnectors
=== 0) {
720 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
722 } else if (templateMaxConnectors
< 0) {
724 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
729 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
730 connectorStatus
.availability
= AvailabilityType
.Operative
731 connectorStatus
.idTagLocalAuthorized
= false
732 connectorStatus
.idTagAuthorized
= false
733 connectorStatus
.transactionRemoteStarted
= false
734 connectorStatus
.transactionStarted
= false
735 connectorStatus
.energyActiveImportRegisterValue
= 0
736 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
737 if (connectorStatus
.chargingProfiles
== null) {
738 connectorStatus
.chargingProfiles
= []
742 const warnDeprecatedTemplateKey
= (
743 template
: ChargingStationTemplate
,
746 templateFile
: string,
749 if (template
?.[key
as keyof ChargingStationTemplate
] !== undefined) {
750 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
751 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
753 logger
.warn(`${logPrefix} ${logMsg}`)
754 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
758 const convertDeprecatedTemplateKey
= (
759 template
: ChargingStationTemplate
,
760 deprecatedKey
: string,
763 if (template
?.[deprecatedKey
as keyof ChargingStationTemplate
] !== undefined) {
764 if (key
!== undefined) {
765 (template
as unknown
as Record
<string, unknown
>)[key
] =
766 template
[deprecatedKey
as keyof ChargingStationTemplate
]
768 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
769 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
773 interface ChargingProfilesLimit
{
775 chargingProfile
: ChargingProfile
779 * Charging profiles shall already be sorted by connector id descending then stack level descending
781 * @param chargingStation -
782 * @param connectorId -
783 * @param chargingProfiles -
785 * @returns ChargingProfilesLimit
787 const getLimitFromChargingProfiles
= (
788 chargingStation
: ChargingStation
,
790 chargingProfiles
: ChargingProfile
[],
792 ): ChargingProfilesLimit
| undefined => {
793 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
794 const currentDate
= new Date()
795 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
796 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!
797 for (const chargingProfile
of chargingProfiles
) {
798 const chargingSchedule
= chargingProfile
.chargingSchedule
799 if (chargingSchedule
?.startSchedule
== null && connectorStatus
?.transactionStarted
=== true) {
801 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
803 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
804 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
806 if (chargingSchedule
?.startSchedule
!= null && !isDate(chargingSchedule
?.startSchedule
)) {
808 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
810 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
811 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
?.startSchedule
)!
813 if (chargingSchedule
?.startSchedule
!= null && chargingSchedule
?.duration
== null) {
815 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
817 // OCPP specifies that if duration is not defined, it should be infinite
818 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
820 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
823 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
826 // Check if the charging profile is active
828 isWithinInterval(currentDate
, {
829 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
830 start
: chargingSchedule
.startSchedule
!,
831 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
832 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!)
835 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
836 const chargingSchedulePeriodCompareFn
= (
837 a
: ChargingSchedulePeriod
,
838 b
: ChargingSchedulePeriod
839 ): number => a
.startPeriod
- b
.startPeriod
841 !isArraySorted
<ChargingSchedulePeriod
>(
842 chargingSchedule
.chargingSchedulePeriod
,
843 chargingSchedulePeriodCompareFn
847 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
849 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
851 // Check if the first schedule period startPeriod property is equal to 0
852 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
854 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
858 // Handle only one schedule period
859 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
860 const result
: ChargingProfilesLimit
= {
861 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
864 logger
.debug(debugLogMsg
, result
)
867 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
868 // Search for the right schedule period
871 chargingSchedulePeriod
872 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
873 // Find the right schedule period
876 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
877 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
881 // Found the schedule period: previous is the correct one
882 const result
: ChargingProfilesLimit
= {
883 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
884 limit
: previousChargingSchedulePeriod
!.limit
,
887 logger
.debug(debugLogMsg
, result
)
890 // Keep a reference to previous one
891 previousChargingSchedulePeriod
= chargingSchedulePeriod
892 // Handle the last schedule period within the charging profile duration
894 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
895 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
898 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
899 chargingSchedule
.startSchedule
!,
900 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
903 chargingSchedule
.startSchedule
!
904 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
905 ) > chargingSchedule
.duration
!)
907 const result
: ChargingProfilesLimit
= {
908 limit
: previousChargingSchedulePeriod
.limit
,
911 logger
.debug(debugLogMsg
, result
)
920 export const prepareChargingProfileKind
= (
921 connectorStatus
: ConnectorStatus
,
922 chargingProfile
: ChargingProfile
,
926 switch (chargingProfile
.chargingProfileKind
) {
927 case ChargingProfileKindType
.RECURRING
:
928 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
931 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
933 case ChargingProfileKindType
.RELATIVE
:
934 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
936 `${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`
938 delete chargingProfile
.chargingSchedule
.startSchedule
940 if (connectorStatus
?.transactionStarted
=== true) {
941 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
943 // FIXME: Handle relative charging profile duration
949 export const canProceedChargingProfile
= (
950 chargingProfile
: ChargingProfile
,
955 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
956 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
957 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
958 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
961 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
962 chargingProfile.chargingProfileId
963 } is not valid for the current date ${currentDate.toISOString()}`
968 chargingProfile
.chargingSchedule
.startSchedule
== null ||
969 chargingProfile
.chargingSchedule
.duration
== null
972 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
977 chargingProfile
.chargingSchedule
.startSchedule
!= null &&
978 !isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)
981 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
986 chargingProfile
.chargingSchedule
.duration
!= null &&
987 !Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)
990 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
997 const canProceedRecurringChargingProfile
= (
998 chargingProfile
: ChargingProfile
,
1002 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1003 chargingProfile
.recurrencyKind
== null
1006 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1011 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1012 chargingProfile
.chargingSchedule
.startSchedule
== null
1015 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1023 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1025 * @param chargingProfile -
1026 * @param currentDate -
1027 * @param logPrefix -
1029 const prepareRecurringChargingProfile
= (
1030 chargingProfile
: ChargingProfile
,
1034 const chargingSchedule
= chargingProfile
.chargingSchedule
1035 let recurringIntervalTranslated
= false
1036 let recurringInterval
: Interval
1037 switch (chargingProfile
.recurrencyKind
) {
1038 case RecurrencyKindType
.DAILY
:
1039 recurringInterval
= {
1040 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1041 start
: chargingSchedule
.startSchedule
!,
1042 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1043 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1045 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1047 !isWithinInterval(currentDate
, recurringInterval
) &&
1048 isBefore(recurringInterval
.end
, currentDate
)
1050 chargingSchedule
.startSchedule
= addDays(
1051 recurringInterval
.start
,
1052 differenceInDays(currentDate
, recurringInterval
.start
)
1054 recurringInterval
= {
1055 start
: chargingSchedule
.startSchedule
,
1056 end
: addDays(chargingSchedule
.startSchedule
, 1)
1058 recurringIntervalTranslated
= true
1061 case RecurrencyKindType
.WEEKLY
:
1062 recurringInterval
= {
1063 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1064 start
: chargingSchedule
.startSchedule
!,
1065 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1066 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1068 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1070 !isWithinInterval(currentDate
, recurringInterval
) &&
1071 isBefore(recurringInterval
.end
, currentDate
)
1073 chargingSchedule
.startSchedule
= addWeeks(
1074 recurringInterval
.start
,
1075 differenceInWeeks(currentDate
, recurringInterval
.start
)
1077 recurringInterval
= {
1078 start
: chargingSchedule
.startSchedule
,
1079 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1081 recurringIntervalTranslated
= true
1086 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1089 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1090 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1092 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1093 chargingProfile.recurrencyKind
1094 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1095 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1096 recurringInterval!.start
1097 ).toISOString()}, ${toDate(
1098 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1099 recurringInterval!.end
1100 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
1103 return recurringIntervalTranslated
1106 const checkRecurringChargingProfileDuration
= (
1107 chargingProfile
: ChargingProfile
,
1111 if (chargingProfile
.chargingSchedule
.duration
== null) {
1113 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1114 chargingProfile.chargingProfileKind
1115 } charging profile id ${
1116 chargingProfile.chargingProfileId
1117 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1122 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1124 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1127 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1128 chargingProfile.chargingProfileKind
1129 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1130 chargingProfile.chargingSchedule.duration
1131 } is greater than the recurrency time interval duration ${differenceInSeconds(
1136 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1140 const getRandomSerialNumberSuffix
= (params
?: {
1141 randomBytesLength
?: number
1144 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1145 if (params
?.upperCase
=== true) {
1146 return randomSerialNumberSuffix
.toUpperCase()
1148 return randomSerialNumberSuffix