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 // 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 (stationTemplate
.idTagsFile
== null || 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 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
294 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
295 const templateMaxAvailableConnectors
=
296 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
298 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
299 stationTemplate
.randomConnectors
!== true
302 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
304 stationTemplate
.randomConnectors
= true
306 return { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
}
309 export const checkStationInfoConnectorStatus
= (
311 connectorStatus
: ConnectorStatus
,
315 if (connectorStatus
.status != null) {
317 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
319 delete connectorStatus
.status
323 export const buildConnectorsMap
= (
324 connectors
: Record
<string, ConnectorStatus
>,
327 ): Map
<number, ConnectorStatus
> => {
328 const connectorsMap
= new Map
<number, ConnectorStatus
>()
329 if (getMaxNumberOfConnectors(connectors
) > 0) {
330 for (const connector
in connectors
) {
331 const connectorStatus
= connectors
[connector
]
332 const connectorId
= convertToInt(connector
)
333 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
334 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
338 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
344 export const initializeConnectorsMapStatus
= (
345 connectors
: Map
<number, ConnectorStatus
>,
348 for (const connectorId
of connectors
.keys()) {
349 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
351 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
352 connectors.get(connectorId)?.transactionId
356 if (connectorId
=== 0) {
357 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
358 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
359 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
360 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
361 connectors
.get(connectorId
)!.chargingProfiles
= []
363 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
364 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
365 initializeConnectorStatus(connectors
.get(connectorId
)!)
370 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
371 if (connectorStatus
== null) {
374 connectorStatus
.chargingProfiles
=
375 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
376 ? connectorStatus
.chargingProfiles
.filter(
377 chargingProfile
=> chargingProfile
.transactionId
!== connectorStatus
.transactionId
380 connectorStatus
.idTagLocalAuthorized
= false
381 connectorStatus
.idTagAuthorized
= false
382 connectorStatus
.transactionRemoteStarted
= false
383 connectorStatus
.transactionStarted
= false
384 delete connectorStatus
.transactionStart
385 delete connectorStatus
.transactionId
386 delete connectorStatus
.localAuthorizeIdTag
387 delete connectorStatus
.authorizeIdTag
388 delete connectorStatus
.transactionIdTag
389 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
390 delete connectorStatus
.transactionBeginMeterValue
393 export const createBootNotificationRequest
= (
394 stationInfo
: ChargingStationInfo
,
395 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
396 ): BootNotificationRequest
| undefined => {
397 const ocppVersion
= stationInfo
.ocppVersion
398 switch (ocppVersion
) {
399 case OCPPVersion
.VERSION_16
:
401 chargePointModel
: stationInfo
.chargePointModel
,
402 chargePointVendor
: stationInfo
.chargePointVendor
,
403 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
404 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
406 ...(stationInfo
.chargePointSerialNumber
!== undefined && {
407 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
409 ...(stationInfo
.firmwareVersion
!== undefined && {
410 firmwareVersion
: stationInfo
.firmwareVersion
412 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
413 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
}),
414 ...(stationInfo
.meterSerialNumber
!== undefined && {
415 meterSerialNumber
: stationInfo
.meterSerialNumber
417 ...(stationInfo
.meterType
!== undefined && {
418 meterType
: stationInfo
.meterType
420 } satisfies OCPP16BootNotificationRequest
421 case OCPPVersion
.VERSION_20
:
422 case OCPPVersion
.VERSION_201
:
426 model
: stationInfo
.chargePointModel
,
427 vendorName
: stationInfo
.chargePointVendor
,
428 ...(stationInfo
.firmwareVersion
!== undefined && {
429 firmwareVersion
: stationInfo
.firmwareVersion
431 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
432 serialNumber
: stationInfo
.chargeBoxSerialNumber
434 ...((stationInfo
.iccid
!== undefined || stationInfo
.imsi
!== undefined) && {
436 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
437 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
})
441 } satisfies OCPP20BootNotificationRequest
445 export const warnTemplateKeysDeprecation
= (
446 stationTemplate
: ChargingStationTemplate
,
450 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
451 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
452 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
453 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
454 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
456 for (const templateKey
of templateKeys
) {
457 warnDeprecatedTemplateKey(
459 templateKey
.deprecatedKey
,
462 templateKey
.key
!== undefined ? `Use '${templateKey.key}' instead` : undefined
464 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
468 export const stationTemplateToStationInfo
= (
469 stationTemplate
: ChargingStationTemplate
470 ): ChargingStationInfo
=> {
471 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
472 delete stationTemplate
.power
473 delete stationTemplate
.powerUnit
474 delete stationTemplate
.Connectors
475 delete stationTemplate
.Evses
476 delete stationTemplate
.Configuration
477 delete stationTemplate
.AutomaticTransactionGenerator
478 delete stationTemplate
.chargeBoxSerialNumberPrefix
479 delete stationTemplate
.chargePointSerialNumberPrefix
480 delete stationTemplate
.meterSerialNumberPrefix
481 return stationTemplate
as ChargingStationInfo
484 export const createSerialNumber
= (
485 stationTemplate
: ChargingStationTemplate
,
486 stationInfo
: ChargingStationInfo
,
488 randomSerialNumberUpperCase
?: boolean
489 randomSerialNumber
?: boolean
492 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
493 const serialNumberSuffix
=
494 params
.randomSerialNumber
=== true
495 ? getRandomSerialNumberSuffix({
496 upperCase
: params
.randomSerialNumberUpperCase
499 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
500 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
501 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
502 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
503 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
504 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
507 export const propagateSerialNumber
= (
508 stationTemplate
: ChargingStationTemplate
| undefined,
509 stationInfoSrc
: ChargingStationInfo
| undefined,
510 stationInfoDst
: ChargingStationInfo
512 if (stationInfoSrc
== null || stationTemplate
== null) {
514 'Missing charging station template or existing configuration to propagate serial number'
517 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
518 stationInfoSrc
.chargePointSerialNumber
!= null
519 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
520 : stationInfoDst
.chargePointSerialNumber
!= null &&
521 delete stationInfoDst
.chargePointSerialNumber
522 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
523 stationInfoSrc
.chargeBoxSerialNumber
!= null
524 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
525 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
526 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
527 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
528 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
531 export const hasFeatureProfile
= (
532 chargingStation
: ChargingStation
,
533 featureProfile
: SupportedFeatureProfiles
534 ): boolean | undefined => {
535 return getConfigurationKey(
537 StandardParametersKey
.SupportedFeatureProfiles
538 )?.value
?.includes(featureProfile
)
541 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
543 switch (stationInfo
.amperageLimitationUnit
) {
544 case AmpereUnits
.DECI_AMPERE
:
547 case AmpereUnits
.CENTI_AMPERE
:
550 case AmpereUnits
.MILLI_AMPERE
:
558 * Gets the connector cloned charging profiles applying a power limitation
559 * and sorted by connector id descending then stack level descending
561 * @param chargingStation -
562 * @param connectorId -
563 * @returns connector charging profiles array
565 export const getConnectorChargingProfiles
= (
566 chargingStation
: ChargingStation
,
568 ): ChargingProfile
[] => {
569 return clone
<ChargingProfile
[]>(
570 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
571 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
573 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
574 (a
, b
) => b
.stackLevel
- a
.stackLevel
580 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
581 chargingStation
: ChargingStation
,
583 ): number | undefined => {
584 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
585 // Get charging profiles sorted by connector id then stack level
586 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
587 if (isNotEmptyArray(chargingProfiles
)) {
588 const result
= getLimitFromChargingProfiles(
592 chargingStation
.logPrefix()
594 if (result
!= null) {
596 chargingProfile
= result
.chargingProfile
597 switch (chargingStation
.stationInfo
?.currentOutType
) {
600 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
602 : ACElectricUtils
.powerTotal(
603 chargingStation
.getNumberOfPhases(),
604 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
605 chargingStation
.stationInfo
.voltageOut
!,
611 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
613 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
614 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
616 const connectorMaximumPower
=
617 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
618 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
619 if (limit
> connectorMaximumPower
) {
621 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
622 chargingProfile.chargingProfileId
623 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
626 limit
= connectorMaximumPower
633 export const getDefaultVoltageOut
= (
634 currentType
: CurrentType
,
638 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
639 let defaultVoltageOut
: number
640 switch (currentType
) {
642 defaultVoltageOut
= Voltage
.VOLTAGE_230
645 defaultVoltageOut
= Voltage
.VOLTAGE_400
648 logger
.error(`${logPrefix} ${errorMsg}`)
649 throw new BaseError(errorMsg
)
651 return defaultVoltageOut
654 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
655 return stationInfo
.idTagsFile
!= null
656 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
660 export const waitChargingStationEvents
= async (
661 emitter
: EventEmitter
,
662 event
: ChargingStationWorkerMessageEvents
,
664 ): Promise
<number> => {
665 return await new Promise
<number>(resolve
=> {
667 if (eventsToWait
=== 0) {
671 emitter
.on(event
, () => {
673 if (events
=== eventsToWait
) {
680 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
681 let configuredMaxNumberOfConnectors
= 0
682 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
683 const numberOfConnectors
= stationTemplate
.numberOfConnectors
684 configuredMaxNumberOfConnectors
=
685 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
686 } else if (stationTemplate
.numberOfConnectors
!= null) {
687 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
688 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
689 configuredMaxNumberOfConnectors
=
690 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
691 stationTemplate
.Connectors
[0] != null
692 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
693 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
694 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
695 for (const evse
in stationTemplate
.Evses
) {
699 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
700 stationTemplate
.Evses
[evse
].Connectors
704 return configuredMaxNumberOfConnectors
707 const checkConfiguredMaxConnectors
= (
708 configuredMaxConnectors
: number,
712 if (configuredMaxConnectors
<= 0) {
714 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
719 const checkTemplateMaxConnectors
= (
720 templateMaxConnectors
: number,
724 if (templateMaxConnectors
=== 0) {
726 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
728 } else if (templateMaxConnectors
< 0) {
730 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
735 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
736 connectorStatus
.availability
= AvailabilityType
.Operative
737 connectorStatus
.idTagLocalAuthorized
= false
738 connectorStatus
.idTagAuthorized
= false
739 connectorStatus
.transactionRemoteStarted
= false
740 connectorStatus
.transactionStarted
= false
741 connectorStatus
.energyActiveImportRegisterValue
= 0
742 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
743 if (connectorStatus
.chargingProfiles
== null) {
744 connectorStatus
.chargingProfiles
= []
748 const warnDeprecatedTemplateKey
= (
749 template
: ChargingStationTemplate
,
752 templateFile
: string,
755 if (template
[key
as keyof ChargingStationTemplate
] !== undefined) {
756 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
757 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
759 logger
.warn(`${logPrefix} ${logMsg}`)
760 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
764 const convertDeprecatedTemplateKey
= (
765 template
: ChargingStationTemplate
,
766 deprecatedKey
: string,
769 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] !== undefined) {
770 if (key
!== undefined) {
771 (template
as unknown
as Record
<string, unknown
>)[key
] =
772 template
[deprecatedKey
as keyof ChargingStationTemplate
]
774 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
775 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
779 interface ChargingProfilesLimit
{
781 chargingProfile
: ChargingProfile
785 * Charging profiles shall already be sorted by connector id descending then stack level descending
787 * @param chargingStation -
788 * @param connectorId -
789 * @param chargingProfiles -
791 * @returns ChargingProfilesLimit
793 const getLimitFromChargingProfiles
= (
794 chargingStation
: ChargingStation
,
796 chargingProfiles
: ChargingProfile
[],
798 ): ChargingProfilesLimit
| undefined => {
799 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
800 const currentDate
= new Date()
801 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
802 for (const chargingProfile
of chargingProfiles
) {
803 const chargingSchedule
= chargingProfile
.chargingSchedule
804 if (chargingSchedule
.startSchedule
== null) {
806 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
808 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
809 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
811 if (!isDate(chargingSchedule
.startSchedule
)) {
813 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
815 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
816 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
818 if (chargingSchedule
.duration
== null) {
820 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
822 // OCPP specifies that if duration is not defined, it should be infinite
823 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
825 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
828 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
831 // Check if the charging profile is active
833 isWithinInterval(currentDate
, {
834 start
: chargingSchedule
.startSchedule
,
835 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
838 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
839 const chargingSchedulePeriodCompareFn
= (
840 a
: ChargingSchedulePeriod
,
841 b
: ChargingSchedulePeriod
842 ): number => a
.startPeriod
- b
.startPeriod
844 !isArraySorted
<ChargingSchedulePeriod
>(
845 chargingSchedule
.chargingSchedulePeriod
,
846 chargingSchedulePeriodCompareFn
850 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
852 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
854 // Check if the first schedule period startPeriod property is equal to 0
855 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
857 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
861 // Handle only one schedule period
862 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
863 const result
: ChargingProfilesLimit
= {
864 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
867 logger
.debug(debugLogMsg
, result
)
870 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
871 // Search for the right schedule period
874 chargingSchedulePeriod
875 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
876 // Find the right schedule period
879 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
883 // Found the schedule period: previous is the correct one
884 const result
: ChargingProfilesLimit
= {
885 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
886 limit
: previousChargingSchedulePeriod
!.limit
,
889 logger
.debug(debugLogMsg
, result
)
892 // Keep a reference to previous one
893 previousChargingSchedulePeriod
= chargingSchedulePeriod
894 // Handle the last schedule period within the charging profile duration
896 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
897 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
900 chargingSchedule
.startSchedule
,
901 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
903 chargingSchedule
.startSchedule
904 ) > chargingSchedule
.duration
)
906 const result
: ChargingProfilesLimit
= {
907 limit
: previousChargingSchedulePeriod
.limit
,
910 logger
.debug(debugLogMsg
, result
)
919 export const prepareChargingProfileKind
= (
920 connectorStatus
: ConnectorStatus
| undefined,
921 chargingProfile
: ChargingProfile
,
922 currentDate
: string | number | Date,
925 switch (chargingProfile
.chargingProfileKind
) {
926 case ChargingProfileKindType
.RECURRING
:
927 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
930 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
932 case ChargingProfileKindType
.RELATIVE
:
933 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
935 `${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`
937 delete chargingProfile
.chargingSchedule
.startSchedule
939 if (connectorStatus
?.transactionStarted
=== true) {
940 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
942 // FIXME: Handle relative charging profile duration
948 export const canProceedChargingProfile
= (
949 chargingProfile
: ChargingProfile
,
950 currentDate
: string | number | Date,
954 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
955 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
958 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
959 chargingProfile.chargingProfileId
960 } is not valid for the current date ${
961 isDate(currentDate) ? currentDate.toISOString() : currentDate
967 chargingProfile
.chargingSchedule
.startSchedule
== null ||
968 chargingProfile
.chargingSchedule
.duration
== null
971 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
975 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
977 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
981 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
983 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
990 const canProceedRecurringChargingProfile
= (
991 chargingProfile
: ChargingProfile
,
995 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
996 chargingProfile
.recurrencyKind
== null
999 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1004 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1005 chargingProfile
.chargingSchedule
.startSchedule
== null
1008 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1016 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1018 * @param chargingProfile -
1019 * @param currentDate -
1020 * @param logPrefix -
1022 const prepareRecurringChargingProfile
= (
1023 chargingProfile
: ChargingProfile
,
1024 currentDate
: string | number | Date,
1027 const chargingSchedule
= chargingProfile
.chargingSchedule
1028 let recurringIntervalTranslated
= false
1029 let recurringInterval
: Interval
| undefined
1030 switch (chargingProfile
.recurrencyKind
) {
1031 case RecurrencyKindType
.DAILY
:
1032 recurringInterval
= {
1033 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1034 start
: chargingSchedule
.startSchedule
!,
1035 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1036 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1038 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1040 !isWithinInterval(currentDate
, recurringInterval
) &&
1041 isBefore(recurringInterval
.end
, currentDate
)
1043 chargingSchedule
.startSchedule
= addDays(
1044 recurringInterval
.start
,
1045 differenceInDays(currentDate
, recurringInterval
.start
)
1047 recurringInterval
= {
1048 start
: chargingSchedule
.startSchedule
,
1049 end
: addDays(chargingSchedule
.startSchedule
, 1)
1051 recurringIntervalTranslated
= true
1054 case RecurrencyKindType
.WEEKLY
:
1055 recurringInterval
= {
1056 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1057 start
: chargingSchedule
.startSchedule
!,
1058 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1059 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1061 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1063 !isWithinInterval(currentDate
, recurringInterval
) &&
1064 isBefore(recurringInterval
.end
, currentDate
)
1066 chargingSchedule
.startSchedule
= addWeeks(
1067 recurringInterval
.start
,
1068 differenceInWeeks(currentDate
, recurringInterval
.start
)
1070 recurringInterval
= {
1071 start
: chargingSchedule
.startSchedule
,
1072 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1074 recurringIntervalTranslated
= true
1079 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1082 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1083 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1085 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1086 chargingProfile.recurrencyKind
1087 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1088 recurringInterval?.start as Date
1089 ).toISOString()}, ${toDate(
1090 recurringInterval?.end as Date
1091 ).toISOString()}] has not been properly translated to current date ${
1092 isDate(currentDate) ? currentDate.toISOString() : currentDate
1096 return recurringIntervalTranslated
1099 const checkRecurringChargingProfileDuration
= (
1100 chargingProfile
: ChargingProfile
,
1104 if (chargingProfile
.chargingSchedule
.duration
== null) {
1106 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1107 chargingProfile.chargingProfileKind
1108 } charging profile id ${
1109 chargingProfile.chargingProfileId
1110 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1115 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1117 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1120 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1121 chargingProfile.chargingProfileKind
1122 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1123 chargingProfile.chargingSchedule.duration
1124 } is greater than the recurrency time interval duration ${differenceInSeconds(
1129 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1133 const getRandomSerialNumberSuffix
= (params
?: {
1134 randomBytesLength
?: number
1137 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1138 if (params
?.upperCase
=== true) {
1139 return randomSerialNumberSuffix
.toUpperCase()
1141 return randomSerialNumberSuffix