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
=> {
394 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
395 const ocppVersion
= stationInfo
.ocppVersion
!
396 switch (ocppVersion
) {
397 case OCPPVersion
.VERSION_16
:
399 chargePointModel
: stationInfo
.chargePointModel
,
400 chargePointVendor
: stationInfo
.chargePointVendor
,
401 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
402 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
404 ...(stationInfo
.chargePointSerialNumber
!== undefined && {
405 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
407 ...(stationInfo
.firmwareVersion
!== undefined && {
408 firmwareVersion
: stationInfo
.firmwareVersion
410 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
411 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
}),
412 ...(stationInfo
.meterSerialNumber
!== undefined && {
413 meterSerialNumber
: stationInfo
.meterSerialNumber
415 ...(stationInfo
.meterType
!== undefined && {
416 meterType
: stationInfo
.meterType
418 } satisfies OCPP16BootNotificationRequest
419 case OCPPVersion
.VERSION_20
:
420 case OCPPVersion
.VERSION_201
:
424 model
: stationInfo
.chargePointModel
,
425 vendorName
: stationInfo
.chargePointVendor
,
426 ...(stationInfo
.firmwareVersion
!== undefined && {
427 firmwareVersion
: stationInfo
.firmwareVersion
429 ...(stationInfo
.chargeBoxSerialNumber
!== undefined && {
430 serialNumber
: stationInfo
.chargeBoxSerialNumber
432 ...((stationInfo
.iccid
!== undefined || stationInfo
.imsi
!== undefined) && {
434 ...(stationInfo
.iccid
!== undefined && { iccid
: stationInfo
.iccid
}),
435 ...(stationInfo
.imsi
!== undefined && { imsi
: stationInfo
.imsi
})
439 } satisfies OCPP20BootNotificationRequest
443 export const warnTemplateKeysDeprecation
= (
444 stationTemplate
: ChargingStationTemplate
,
448 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
449 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
450 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
451 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
452 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
454 for (const templateKey
of templateKeys
) {
455 warnDeprecatedTemplateKey(
457 templateKey
.deprecatedKey
,
460 templateKey
.key
!== undefined ? `Use '${templateKey.key}' instead` : undefined
462 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
466 export const stationTemplateToStationInfo
= (
467 stationTemplate
: ChargingStationTemplate
468 ): ChargingStationInfo
=> {
469 stationTemplate
= cloneObject
<ChargingStationTemplate
>(stationTemplate
)
470 delete stationTemplate
.power
471 delete stationTemplate
.powerUnit
472 delete stationTemplate
.Connectors
473 delete stationTemplate
.Evses
474 delete stationTemplate
.Configuration
475 delete stationTemplate
.AutomaticTransactionGenerator
476 delete stationTemplate
.chargeBoxSerialNumberPrefix
477 delete stationTemplate
.chargePointSerialNumberPrefix
478 delete stationTemplate
.meterSerialNumberPrefix
479 return stationTemplate
as ChargingStationInfo
482 export const createSerialNumber
= (
483 stationTemplate
: ChargingStationTemplate
,
484 stationInfo
: ChargingStationInfo
,
486 randomSerialNumberUpperCase
?: boolean
487 randomSerialNumber
?: boolean
490 params
= { ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true }, ...params
}
491 const serialNumberSuffix
=
492 params
.randomSerialNumber
=== true
493 ? getRandomSerialNumberSuffix({
494 upperCase
: params
.randomSerialNumberUpperCase
497 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
498 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
499 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
500 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
501 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
502 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
505 export const propagateSerialNumber
= (
506 stationTemplate
: ChargingStationTemplate
| undefined,
507 stationInfoSrc
: ChargingStationInfo
| undefined,
508 stationInfoDst
: ChargingStationInfo
510 if (stationInfoSrc
== null || stationTemplate
== null) {
512 'Missing charging station template or existing configuration to propagate serial number'
515 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
516 stationInfoSrc
.chargePointSerialNumber
!= null
517 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
518 : stationInfoDst
.chargePointSerialNumber
!= null &&
519 delete stationInfoDst
.chargePointSerialNumber
520 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
521 stationInfoSrc
.chargeBoxSerialNumber
!= null
522 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
523 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
524 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
525 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
526 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
529 export const hasFeatureProfile
= (
530 chargingStation
: ChargingStation
,
531 featureProfile
: SupportedFeatureProfiles
532 ): boolean | undefined => {
533 return getConfigurationKey(
535 StandardParametersKey
.SupportedFeatureProfiles
536 )?.value
?.includes(featureProfile
)
539 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
541 switch (stationInfo
.amperageLimitationUnit
) {
542 case AmpereUnits
.DECI_AMPERE
:
545 case AmpereUnits
.CENTI_AMPERE
:
548 case AmpereUnits
.MILLI_AMPERE
:
556 * Gets the connector cloned charging profiles applying a power limitation
557 * and sorted by connector id descending then stack level descending
559 * @param chargingStation -
560 * @param connectorId -
561 * @returns connector charging profiles array
563 export const getConnectorChargingProfiles
= (
564 chargingStation
: ChargingStation
,
566 ): ChargingProfile
[] => {
567 return cloneObject
<ChargingProfile
[]>(
568 (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
569 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
571 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? []).sort(
572 (a
, b
) => b
.stackLevel
- a
.stackLevel
578 export const getChargingStationConnectorChargingProfilesPowerLimit
= (
579 chargingStation
: ChargingStation
,
581 ): number | undefined => {
582 let limit
: number | undefined, chargingProfile
: ChargingProfile
| undefined
583 // Get charging profiles sorted by connector id then stack level
584 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
585 if (isNotEmptyArray(chargingProfiles
)) {
586 const result
= getLimitFromChargingProfiles(
590 chargingStation
.logPrefix()
592 if (result
!= null) {
594 chargingProfile
= result
.chargingProfile
595 switch (chargingStation
.stationInfo
?.currentOutType
) {
598 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
600 : ACElectricUtils
.powerTotal(
601 chargingStation
.getNumberOfPhases(),
602 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
603 chargingStation
.stationInfo
.voltageOut
!,
609 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
611 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
612 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
614 const connectorMaximumPower
=
615 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
616 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
617 if (limit
> connectorMaximumPower
) {
619 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
620 chargingProfile.chargingProfileId
621 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
624 limit
= connectorMaximumPower
631 export const getDefaultVoltageOut
= (
632 currentType
: CurrentType
,
636 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
637 let defaultVoltageOut
: number
638 switch (currentType
) {
640 defaultVoltageOut
= Voltage
.VOLTAGE_230
643 defaultVoltageOut
= Voltage
.VOLTAGE_400
646 logger
.error(`${logPrefix} ${errorMsg}`)
647 throw new BaseError(errorMsg
)
649 return defaultVoltageOut
652 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
653 return stationInfo
.idTagsFile
!= null
654 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
658 export const waitChargingStationEvents
= async (
659 emitter
: EventEmitter
,
660 event
: ChargingStationWorkerMessageEvents
,
662 ): Promise
<number> => {
663 return await new Promise
<number>((resolve
) => {
665 if (eventsToWait
=== 0) {
669 emitter
.on(event
, () => {
671 if (events
=== eventsToWait
) {
678 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
679 let configuredMaxNumberOfConnectors
= 0
680 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
681 const numberOfConnectors
= stationTemplate
.numberOfConnectors
as number[]
682 configuredMaxNumberOfConnectors
=
683 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
684 } else if (stationTemplate
.numberOfConnectors
!= null) {
685 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number
686 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
687 configuredMaxNumberOfConnectors
=
688 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
689 stationTemplate
.Connectors
[0] != null
690 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
691 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
692 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
693 for (const evse
in stationTemplate
.Evses
) {
697 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
698 stationTemplate
.Evses
[evse
].Connectors
702 return configuredMaxNumberOfConnectors
705 const checkConfiguredMaxConnectors
= (
706 configuredMaxConnectors
: number,
710 if (configuredMaxConnectors
<= 0) {
712 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
717 const checkTemplateMaxConnectors
= (
718 templateMaxConnectors
: number,
722 if (templateMaxConnectors
=== 0) {
724 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
726 } else if (templateMaxConnectors
< 0) {
728 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
733 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
734 connectorStatus
.availability
= AvailabilityType
.Operative
735 connectorStatus
.idTagLocalAuthorized
= false
736 connectorStatus
.idTagAuthorized
= false
737 connectorStatus
.transactionRemoteStarted
= false
738 connectorStatus
.transactionStarted
= false
739 connectorStatus
.energyActiveImportRegisterValue
= 0
740 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
741 if (connectorStatus
.chargingProfiles
== null) {
742 connectorStatus
.chargingProfiles
= []
746 const warnDeprecatedTemplateKey
= (
747 template
: ChargingStationTemplate
,
750 templateFile
: string,
753 if (template
[key
as keyof ChargingStationTemplate
] !== undefined) {
754 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
755 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
757 logger
.warn(`${logPrefix} ${logMsg}`)
758 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
762 const convertDeprecatedTemplateKey
= (
763 template
: ChargingStationTemplate
,
764 deprecatedKey
: string,
767 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] !== undefined) {
768 if (key
!== undefined) {
769 (template
as unknown
as Record
<string, unknown
>)[key
] =
770 template
[deprecatedKey
as keyof ChargingStationTemplate
]
772 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
773 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
777 interface ChargingProfilesLimit
{
779 chargingProfile
: ChargingProfile
783 * Charging profiles shall already be sorted by connector id descending then stack level descending
785 * @param chargingStation -
786 * @param connectorId -
787 * @param chargingProfiles -
789 * @returns ChargingProfilesLimit
791 const getLimitFromChargingProfiles
= (
792 chargingStation
: ChargingStation
,
794 chargingProfiles
: ChargingProfile
[],
796 ): ChargingProfilesLimit
| undefined => {
797 const debugLogMsg
= `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
798 const currentDate
= new Date()
799 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
800 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)!
801 for (const chargingProfile
of chargingProfiles
) {
802 const chargingSchedule
= chargingProfile
.chargingSchedule
803 if (chargingSchedule
.startSchedule
== null && connectorStatus
.transactionStarted
=== true) {
805 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
807 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
808 chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
810 if (chargingSchedule
.startSchedule
!= null && !isDate(chargingSchedule
.startSchedule
)) {
812 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
814 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
815 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
817 if (chargingSchedule
.startSchedule
!= null && chargingSchedule
.duration
== null) {
819 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
821 // OCPP specifies that if duration is not defined, it should be infinite
822 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
824 if (!prepareChargingProfileKind(connectorStatus
, chargingProfile
, currentDate
, logPrefix
)) {
827 if (!canProceedChargingProfile(chargingProfile
, currentDate
, logPrefix
)) {
830 // Check if the charging profile is active
832 isWithinInterval(currentDate
, {
833 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
834 start
: chargingSchedule
.startSchedule
!,
835 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
836 end
: addSeconds(chargingSchedule
.startSchedule
!, chargingSchedule
.duration
!)
839 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
840 const chargingSchedulePeriodCompareFn
= (
841 a
: ChargingSchedulePeriod
,
842 b
: ChargingSchedulePeriod
843 ): number => a
.startPeriod
- b
.startPeriod
845 !isArraySorted
<ChargingSchedulePeriod
>(
846 chargingSchedule
.chargingSchedulePeriod
,
847 chargingSchedulePeriodCompareFn
851 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
853 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
855 // Check if the first schedule period startPeriod property is equal to 0
856 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
858 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
862 // Handle only one schedule period
863 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
864 const result
: ChargingProfilesLimit
= {
865 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
868 logger
.debug(debugLogMsg
, result
)
871 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
872 // Search for the right schedule period
875 chargingSchedulePeriod
876 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
877 // Find the right schedule period
880 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
881 addSeconds(chargingSchedule
.startSchedule
!, chargingSchedulePeriod
.startPeriod
),
885 // Found the schedule period: previous is the correct one
886 const result
: ChargingProfilesLimit
= {
887 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
888 limit
: previousChargingSchedulePeriod
!.limit
,
891 logger
.debug(debugLogMsg
, result
)
894 // Keep a reference to previous one
895 previousChargingSchedulePeriod
= chargingSchedulePeriod
896 // Handle the last schedule period within the charging profile duration
898 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
899 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
903 chargingSchedule
.startSchedule
!,
904 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
906 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
907 chargingSchedule
.startSchedule
!
908 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
909 ) > chargingSchedule
.duration
!)
911 const result
: ChargingProfilesLimit
= {
912 limit
: previousChargingSchedulePeriod
.limit
,
915 logger
.debug(debugLogMsg
, result
)
924 export const prepareChargingProfileKind
= (
925 connectorStatus
: ConnectorStatus
,
926 chargingProfile
: ChargingProfile
,
930 switch (chargingProfile
.chargingProfileKind
) {
931 case ChargingProfileKindType
.RECURRING
:
932 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
935 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
937 case ChargingProfileKindType
.RELATIVE
:
938 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
940 `${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`
942 delete chargingProfile
.chargingSchedule
.startSchedule
944 if (connectorStatus
.transactionStarted
=== true) {
945 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
947 // FIXME: Handle relative charging profile duration
953 export const canProceedChargingProfile
= (
954 chargingProfile
: ChargingProfile
,
959 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
960 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
961 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
962 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
965 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
966 chargingProfile.chargingProfileId
967 } is not valid for the current date ${currentDate.toISOString()}`
972 chargingProfile
.chargingSchedule
.startSchedule
== null ||
973 chargingProfile
.chargingSchedule
.duration
== null
976 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
980 if (!isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)) {
982 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
986 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
988 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
995 const canProceedRecurringChargingProfile
= (
996 chargingProfile
: ChargingProfile
,
1000 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1001 chargingProfile
.recurrencyKind
== null
1004 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1009 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1010 chargingProfile
.chargingSchedule
.startSchedule
== null
1013 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1021 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1023 * @param chargingProfile -
1024 * @param currentDate -
1025 * @param logPrefix -
1027 const prepareRecurringChargingProfile
= (
1028 chargingProfile
: ChargingProfile
,
1032 const chargingSchedule
= chargingProfile
.chargingSchedule
1033 let recurringIntervalTranslated
= false
1034 let recurringInterval
: Interval
1035 switch (chargingProfile
.recurrencyKind
) {
1036 case RecurrencyKindType
.DAILY
:
1037 recurringInterval
= {
1038 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1039 start
: chargingSchedule
.startSchedule
!,
1040 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1041 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1043 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1045 !isWithinInterval(currentDate
, recurringInterval
) &&
1046 isBefore(recurringInterval
.end
, currentDate
)
1048 chargingSchedule
.startSchedule
= addDays(
1049 recurringInterval
.start
,
1050 differenceInDays(currentDate
, recurringInterval
.start
)
1052 recurringInterval
= {
1053 start
: chargingSchedule
.startSchedule
,
1054 end
: addDays(chargingSchedule
.startSchedule
, 1)
1056 recurringIntervalTranslated
= true
1059 case RecurrencyKindType
.WEEKLY
:
1060 recurringInterval
= {
1061 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1062 start
: chargingSchedule
.startSchedule
!,
1063 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1064 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1066 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1068 !isWithinInterval(currentDate
, recurringInterval
) &&
1069 isBefore(recurringInterval
.end
, currentDate
)
1071 chargingSchedule
.startSchedule
= addWeeks(
1072 recurringInterval
.start
,
1073 differenceInWeeks(currentDate
, recurringInterval
.start
)
1075 recurringInterval
= {
1076 start
: chargingSchedule
.startSchedule
,
1077 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1079 recurringIntervalTranslated
= true
1084 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1087 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1088 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1090 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1091 chargingProfile.recurrencyKind
1092 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1093 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1094 recurringInterval!.start
1095 ).toISOString()}, ${toDate(
1096 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1097 recurringInterval!.end
1098 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
1101 return recurringIntervalTranslated
1104 const checkRecurringChargingProfileDuration
= (
1105 chargingProfile
: ChargingProfile
,
1109 if (chargingProfile
.chargingSchedule
.duration
== null) {
1111 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1112 chargingProfile.chargingProfileKind
1113 } charging profile id ${
1114 chargingProfile.chargingProfileId
1115 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1120 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1122 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1125 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1126 chargingProfile.chargingProfileKind
1127 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1128 chargingProfile.chargingSchedule.duration
1129 } is greater than the recurrency time interval duration ${differenceInSeconds(
1134 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1138 const getRandomSerialNumberSuffix
= (params
?: {
1139 randomBytesLength
?: number
1142 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1143 if (params
?.upperCase
=== true) {
1144 return randomSerialNumberSuffix
.toUpperCase()
1146 return randomSerialNumberSuffix