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
, cloneObject
<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 ${connectors.get(
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
): void => {
371 connectorStatus
.chargingProfiles
=
372 connectorStatus
.transactionId
!= null && isNotEmptyArray(connectorStatus
.chargingProfiles
)
373 ? connectorStatus
.chargingProfiles
?.filter(
374 (chargingProfile
) => chargingProfile
.transactionId
!== connectorStatus
.transactionId
377 connectorStatus
.idTagLocalAuthorized
= false
378 connectorStatus
.idTagAuthorized
= false
379 connectorStatus
.transactionRemoteStarted
= false
380 connectorStatus
.transactionStarted
= false
381 delete connectorStatus
.transactionStart
382 delete connectorStatus
.transactionId
383 delete connectorStatus
.localAuthorizeIdTag
384 delete connectorStatus
.authorizeIdTag
385 delete connectorStatus
.transactionIdTag
386 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
387 delete connectorStatus
.transactionBeginMeterValue
390 export const createBootNotificationRequest
= (
391 stationInfo
: ChargingStationInfo
,
392 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
393 ): BootNotificationRequest
| undefined => {
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
| undefined,
506 stationInfoSrc
: ChargingStationInfo
| undefined,
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) {
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 ${
619 chargingProfile.chargingProfileId
620 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
623 limit
= connectorMaximumPower
630 export const getDefaultVoltageOut
= (
631 currentType
: CurrentType
,
635 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
636 let defaultVoltageOut
: number
637 switch (currentType
) {
639 defaultVoltageOut
= Voltage
.VOLTAGE_230
642 defaultVoltageOut
= Voltage
.VOLTAGE_400
645 logger
.error(`${logPrefix} ${errorMsg}`)
646 throw new BaseError(errorMsg
)
648 return defaultVoltageOut
651 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
652 return stationInfo
.idTagsFile
!= null
653 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
657 export const waitChargingStationEvents
= async (
658 emitter
: EventEmitter
,
659 event
: ChargingStationWorkerMessageEvents
,
661 ): Promise
<number> => {
662 return await new Promise
<number>((resolve
) => {
664 if (eventsToWait
=== 0) {
668 emitter
.on(event
, () => {
670 if (events
=== eventsToWait
) {
677 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
678 let configuredMaxNumberOfConnectors
= 0
679 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
680 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[]
681 configuredMaxNumberOfConnectors
=
682 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
683 } else if (stationTemplate
.numberOfConnectors
!= null) {
684 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number
685 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
686 configuredMaxNumberOfConnectors
=
687 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
688 stationTemplate
.Connectors
[0] != null
689 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
690 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
691 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
692 for (const evse
in stationTemplate
.Evses
) {
696 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
697 stationTemplate
.Evses
[evse
].Connectors
701 return configuredMaxNumberOfConnectors
704 const checkConfiguredMaxConnectors
= (
705 configuredMaxConnectors
: number,
709 if (configuredMaxConnectors
<= 0) {
711 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
716 const checkTemplateMaxConnectors
= (
717 templateMaxConnectors
: number,
721 if (templateMaxConnectors
=== 0) {
723 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
725 } else if (templateMaxConnectors
< 0) {
727 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
732 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
733 connectorStatus
.availability
= AvailabilityType
.Operative
734 connectorStatus
.idTagLocalAuthorized
= false
735 connectorStatus
.idTagAuthorized
= false
736 connectorStatus
.transactionRemoteStarted
= false
737 connectorStatus
.transactionStarted
= false
738 connectorStatus
.energyActiveImportRegisterValue
= 0
739 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
740 if (connectorStatus
.chargingProfiles
== null) {
741 connectorStatus
.chargingProfiles
= []
745 const warnDeprecatedTemplateKey
= (
746 template
: ChargingStationTemplate
,
749 templateFile
: string,
752 if (template
[key
as keyof ChargingStationTemplate
] !== undefined) {
753 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
754 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
756 logger
.warn(`${logPrefix} ${logMsg}`)
757 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
761 const convertDeprecatedTemplateKey
= (
762 template
: ChargingStationTemplate
,
763 deprecatedKey
: string,
766 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] !== undefined) {
767 if (key
!== undefined) {
768 (template
as unknown
as Record
<string, unknown
>)[key
] =
769 template
[deprecatedKey
as keyof ChargingStationTemplate
]
771 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
772 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
776 interface ChargingProfilesLimit
{
778 chargingProfile
: ChargingProfile
782 * Charging profiles shall already be sorted by connector id descending then stack level descending
784 * @param chargingStation -
785 * @param connectorId -
786 * @param chargingProfiles -
788 * @returns ChargingProfilesLimit
790 const getLimitFromChargingProfiles
= (
791 chargingStation
: ChargingStation
,
793 chargingProfiles
: ChargingProfile
[],
795 ): ChargingProfilesLimit
| undefined => {
796 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
797 const currentDate
= new Date()
798 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
799 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!
800 for (const chargingProfile
of chargingProfiles
) {
801 const chargingSchedule
= chargingProfile
.chargingSchedule
802 if (chargingSchedule
.startSchedule
== null) {
804 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
806 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
807 chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
809 if (!isDate(chargingSchedule
.startSchedule
)) {
811 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
813 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
814 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
816 if (chargingSchedule
.duration
== null) {
818 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
820 // OCPP specifies that if duration is not defined, it should be infinite
821 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
823 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
826 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
829 // Check if the charging profile is active
831 isWithinInterval(currentDate
, {
832 start
: chargingSchedule
.startSchedule
,
833 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
836 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
837 const chargingSchedulePeriodCompareFn
= (
838 a
: ChargingSchedulePeriod
,
839 b
: ChargingSchedulePeriod
840 ): number => a
.startPeriod
- b
.startPeriod
842 !isArraySorted
<ChargingSchedulePeriod
>(
843 chargingSchedule
.chargingSchedulePeriod
,
844 chargingSchedulePeriodCompareFn
848 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
850 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
852 // Check if the first schedule period startPeriod property is equal to 0
853 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
855 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
859 // Handle only one schedule period
860 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
861 const result
: ChargingProfilesLimit
= {
862 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
865 logger
.debug(debugLogMsg
, result
)
868 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
869 // Search for the right schedule period
872 chargingSchedulePeriod
873 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
874 // Find the right schedule period
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 chargingSchedule
.startSchedule
,
899 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
901 chargingSchedule
.startSchedule
902 ) > chargingSchedule
.duration
)
904 const result
: ChargingProfilesLimit
= {
905 limit
: previousChargingSchedulePeriod
.limit
,
908 logger
.debug(debugLogMsg
, result
)
917 export const prepareChargingProfileKind
= (
918 connectorStatus
: ConnectorStatus
,
919 chargingProfile
: ChargingProfile
,
920 currentDate
: string | number | Date,
923 switch (chargingProfile
.chargingProfileKind
) {
924 case ChargingProfileKindType
.RECURRING
:
925 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
928 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
930 case ChargingProfileKindType
.RELATIVE
:
931 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
933 `${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`
935 delete chargingProfile
.chargingSchedule
.startSchedule
937 if (connectorStatus
.transactionStarted
=== true) {
938 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
940 // FIXME: Handle relative charging profile duration
946 export const canProceedChargingProfile
= (
947 chargingProfile
: ChargingProfile
,
948 currentDate
: string | number | Date,
952 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
953 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
954 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
955 (isValidTime(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 currentDate instanceof Date ? 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 (!isValidTime(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
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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1089 recurringInterval!.start
1090 ).toISOString()}, ${toDate(
1091 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1092 recurringInterval!.end
1093 ).toISOString()}] has not been properly translated to current date ${
1094 currentDate instanceof Date ? currentDate.toISOString() : currentDate
1098 return recurringIntervalTranslated
1101 const checkRecurringChargingProfileDuration
= (
1102 chargingProfile
: ChargingProfile
,
1106 if (chargingProfile
.chargingSchedule
.duration
== null) {
1108 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1109 chargingProfile.chargingProfileKind
1110 } charging profile id ${
1111 chargingProfile.chargingProfileId
1112 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1117 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1119 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1122 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1123 chargingProfile.chargingProfileKind
1124 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1125 chargingProfile.chargingSchedule.duration
1126 } is greater than the recurrency time interval duration ${differenceInSeconds(
1131 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1135 const getRandomSerialNumberSuffix
= (params
?: {
1136 randomBytesLength
?: number
1139 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1140 if (params
?.upperCase
=== true) {
1141 return randomSerialNumberSuffix
.toUpperCase()
1143 return randomSerialNumberSuffix