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 ${
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
= cloneObject
<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 cloneObject
<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
as number[]
684 configuredMaxNumberOfConnectors
=
685 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
686 } else if (stationTemplate
.numberOfConnectors
!= null) {
687 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
as number
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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
955 (isValidTime(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
!)) ||
956 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
957 (isValidTime(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
!))
960 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
961 chargingProfile.chargingProfileId
962 } is not valid for the current date ${
963 currentDate instanceof Date ? currentDate.toISOString() : currentDate
969 chargingProfile
.chargingSchedule
.startSchedule
== null ||
970 chargingProfile
.chargingSchedule
.duration
== null
973 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
977 if (!isValidTime(chargingProfile
.chargingSchedule
.startSchedule
)) {
979 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
983 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
985 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
992 const canProceedRecurringChargingProfile
= (
993 chargingProfile
: ChargingProfile
,
997 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
998 chargingProfile
.recurrencyKind
== null
1001 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1006 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1007 chargingProfile
.chargingSchedule
.startSchedule
== null
1010 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1018 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1020 * @param chargingProfile -
1021 * @param currentDate -
1022 * @param logPrefix -
1024 const prepareRecurringChargingProfile
= (
1025 chargingProfile
: ChargingProfile
,
1026 currentDate
: string | number | Date,
1029 const chargingSchedule
= chargingProfile
.chargingSchedule
1030 let recurringIntervalTranslated
= false
1031 let recurringInterval
: Interval
1032 switch (chargingProfile
.recurrencyKind
) {
1033 case RecurrencyKindType
.DAILY
:
1034 recurringInterval
= {
1035 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1036 start
: chargingSchedule
.startSchedule
!,
1037 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1038 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1040 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1042 !isWithinInterval(currentDate
, recurringInterval
) &&
1043 isBefore(recurringInterval
.end
, currentDate
)
1045 chargingSchedule
.startSchedule
= addDays(
1046 recurringInterval
.start
,
1047 differenceInDays(currentDate
, recurringInterval
.start
)
1049 recurringInterval
= {
1050 start
: chargingSchedule
.startSchedule
,
1051 end
: addDays(chargingSchedule
.startSchedule
, 1)
1053 recurringIntervalTranslated
= true
1056 case RecurrencyKindType
.WEEKLY
:
1057 recurringInterval
= {
1058 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1059 start
: chargingSchedule
.startSchedule
!,
1060 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1061 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1063 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1065 !isWithinInterval(currentDate
, recurringInterval
) &&
1066 isBefore(recurringInterval
.end
, currentDate
)
1068 chargingSchedule
.startSchedule
= addWeeks(
1069 recurringInterval
.start
,
1070 differenceInWeeks(currentDate
, recurringInterval
.start
)
1072 recurringInterval
= {
1073 start
: chargingSchedule
.startSchedule
,
1074 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1076 recurringIntervalTranslated
= true
1081 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1084 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1085 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1087 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1088 chargingProfile.recurrencyKind
1089 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1090 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1091 recurringInterval!.start
1092 ).toISOString()}, ${toDate(
1093 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1094 recurringInterval!.end
1095 ).toISOString()}] has not been properly translated to current date ${
1096 currentDate instanceof Date ? currentDate.toISOString() : currentDate
1100 return recurringIntervalTranslated
1103 const checkRecurringChargingProfileDuration
= (
1104 chargingProfile
: ChargingProfile
,
1108 if (chargingProfile
.chargingSchedule
.duration
== null) {
1110 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1111 chargingProfile.chargingProfileKind
1112 } charging profile id ${
1113 chargingProfile.chargingProfileId
1114 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1119 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1121 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1124 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1125 chargingProfile.chargingProfileKind
1126 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1127 chargingProfile.chargingSchedule.duration
1128 } is greater than the recurrency time interval duration ${differenceInSeconds(
1133 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1137 const getRandomSerialNumberSuffix
= (params
?: {
1138 randomBytesLength
?: number
1141 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1142 if (params
?.upperCase
=== true) {
1143 return randomSerialNumberSuffix
.toUpperCase()
1145 return randomSerialNumberSuffix