1 import { createHash
, randomBytes
} from
'node:crypto'
2 import type { EventEmitter
} from
'node:events'
3 import { basename
, dirname
, isAbsolute
, join
, parse
, relative
, resolve
} 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'
24 import { isEmpty
} from
'rambda'
26 import { BaseError
} from
'../exception/index.js'
30 type BootNotificationRequest
,
33 ChargingProfileKindType
,
34 ChargingProfilePurposeType
,
36 type ChargingSchedulePeriod
,
37 type ChargingStationConfiguration
,
38 type ChargingStationInfo
,
39 type ChargingStationOptions
,
40 type ChargingStationTemplate
,
41 type ChargingStationWorkerMessageEvents
,
42 ConnectorPhaseRotation
,
47 type OCPP16BootNotificationRequest
,
48 type OCPP20BootNotificationRequest
,
52 ReservationTerminationReason
,
53 StandardParametersKey
,
54 type SupportedFeatureProfiles
,
56 } from
'../types/index.js'
70 } from
'../utils/index.js'
71 import type { ChargingStation
} from
'./ChargingStation.js'
72 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
74 const moduleName
= 'Helpers'
76 export const buildTemplateName
= (templateFile
: string): string => {
77 if (isAbsolute(templateFile
)) {
78 templateFile
= relative(
79 resolve(join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'station-templates')),
83 const templateFileParsedPath
= parse(templateFile
)
84 return join(templateFileParsedPath
.dir
, templateFileParsedPath
.name
)
87 export const getChargingStationId
= (
89 stationTemplate
: ChargingStationTemplate
| undefined
91 if (stationTemplate
== null) {
92 return "Unknown 'chargingStationId'"
94 // In case of multiple instances: add instance index to charging station id
95 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
96 const idSuffix
= stationTemplate
.nameSuffix
?? ''
97 const idStr
= `000000000${index.toString()}`
98 return stationTemplate
.fixedName
=== true
99 ? stationTemplate
.baseName
100 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
105 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
106 return isPast(reservation
.expiryDate
)
109 export const removeExpiredReservations
= async (
110 chargingStation
: ChargingStation
111 ): Promise
<void> => {
112 if (chargingStation
.hasEvses
) {
113 for (const evseStatus
of chargingStation
.evses
.values()) {
114 for (const connectorStatus
of evseStatus
.connectors
.values()) {
116 connectorStatus
.reservation
!= null &&
117 hasReservationExpired(connectorStatus
.reservation
)
119 await chargingStation
.removeReservation(
120 connectorStatus
.reservation
,
121 ReservationTerminationReason
.EXPIRED
127 for (const connectorStatus
of chargingStation
.connectors
.values()) {
129 connectorStatus
.reservation
!= null &&
130 hasReservationExpired(connectorStatus
.reservation
)
132 await chargingStation
.removeReservation(
133 connectorStatus
.reservation
,
134 ReservationTerminationReason
.EXPIRED
141 export const getNumberOfReservableConnectors
= (
142 connectors
: Map
<number, ConnectorStatus
>
144 let numberOfReservableConnectors
= 0
145 for (const [connectorId
, connectorStatus
] of connectors
) {
146 if (connectorId
=== 0) {
149 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
150 ++numberOfReservableConnectors
153 return numberOfReservableConnectors
156 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
157 const chargingStationInfo
= {
158 chargePointModel
: stationTemplate
.chargePointModel
,
159 chargePointVendor
: stationTemplate
.chargePointVendor
,
160 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!= null && {
161 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
163 ...(stationTemplate
.chargePointSerialNumberPrefix
!= null && {
164 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
166 ...(stationTemplate
.meterSerialNumberPrefix
!= null && {
167 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
169 ...(stationTemplate
.meterType
!= null && {
170 meterType
: stationTemplate
.meterType
173 return createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
174 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
178 export const checkChargingStation
= (
179 chargingStation
: ChargingStation
,
182 if (!chargingStation
.started
&& !chargingStation
.starting
) {
183 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
189 export const getPhaseRotationValue
= (
191 numberOfPhases
: number
192 ): string | undefined => {
194 if (connectorId
=== 0 && numberOfPhases
=== 0) {
195 return `${connectorId}.${ConnectorPhaseRotation.RST}`
196 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
197 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
199 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
200 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
201 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
202 return `${connectorId}.${ConnectorPhaseRotation.RST}`
206 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
210 return Object.keys(evses
).length
213 const getMaxNumberOfConnectors
= (
214 connectors
: Record
<string, ConnectorStatus
> | undefined
216 if (connectors
== null) {
219 return Object.keys(connectors
).length
222 export const getBootConnectorStatus
= (
223 chargingStation
: ChargingStation
,
225 connectorStatus
: ConnectorStatus
226 ): ConnectorStatusEnum
=> {
227 let connectorBootStatus
: ConnectorStatusEnum
229 connectorStatus
.status == null &&
230 (!chargingStation
.isChargingStationAvailable() ||
231 !chargingStation
.isConnectorAvailable(connectorId
))
233 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
234 } else if (connectorStatus
.status == null && connectorStatus
.bootStatus
!= null) {
235 // Set boot status in template at startup
236 connectorBootStatus
= connectorStatus
.bootStatus
237 } else if (connectorStatus
.status != null) {
238 // Set previous status at startup
239 connectorBootStatus
= connectorStatus
.status
241 // Set default status
242 connectorBootStatus
= ConnectorStatusEnum
.Available
244 return connectorBootStatus
247 export const checkTemplate
= (
248 stationTemplate
: ChargingStationTemplate
| undefined,
252 if (stationTemplate
== null) {
253 const errorMsg
= `Failed to read charging station template file ${templateFile}`
254 logger
.error(`${logPrefix} ${errorMsg}`)
255 throw new BaseError(errorMsg
)
257 if (isEmpty(stationTemplate
)) {
258 const errorMsg
= `Empty charging station information from template file ${templateFile}`
259 logger
.error(`${logPrefix} ${errorMsg}`)
260 throw new BaseError(errorMsg
)
262 if (stationTemplate
.idTagsFile
== null || isEmpty(stationTemplate
.idTagsFile
)) {
264 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
269 export const checkConfiguration
= (
270 stationConfiguration
: ChargingStationConfiguration
| undefined,
272 configurationFile
: string
274 if (stationConfiguration
== null) {
275 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
276 logger
.error(`${logPrefix} ${errorMsg}`)
277 throw new BaseError(errorMsg
)
279 if (isEmpty(stationConfiguration
)) {
280 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
281 logger
.error(`${logPrefix} ${errorMsg}`)
282 throw new BaseError(errorMsg
)
286 export const checkConnectorsConfiguration
= (
287 stationTemplate
: ChargingStationTemplate
,
291 configuredMaxConnectors
: number
292 templateMaxConnectors
: number
293 templateMaxAvailableConnectors
: number
295 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
296 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
297 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
298 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
299 const templateMaxAvailableConnectors
=
300 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
302 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
303 stationTemplate
.randomConnectors
!== true
306 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
308 stationTemplate
.randomConnectors
= true
311 configuredMaxConnectors
,
312 templateMaxConnectors
,
313 templateMaxAvailableConnectors
317 export const checkStationInfoConnectorStatus
= (
319 connectorStatus
: ConnectorStatus
,
323 if (connectorStatus
.status != null) {
325 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
327 delete connectorStatus
.status
331 export const setChargingStationOptions
= (
332 stationInfo
: ChargingStationInfo
,
333 options
?: ChargingStationOptions
334 ): ChargingStationInfo
=> {
335 if (options
?.supervisionUrls
!= null) {
336 stationInfo
.supervisionUrls
= options
.supervisionUrls
338 if (options
?.persistentConfiguration
!= null) {
339 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
340 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
341 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
342 options
.persistentConfiguration
344 if (options
?.autoStart
!= null) {
345 stationInfo
.autoStart
= options
.autoStart
347 if (options
?.autoRegister
!= null) {
348 stationInfo
.autoRegister
= options
.autoRegister
350 if (options
?.enableStatistics
!= null) {
351 stationInfo
.enableStatistics
= options
.enableStatistics
353 if (options
?.ocppStrictCompliance
!= null) {
354 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
356 if (options
?.stopTransactionsOnStopped
!= null) {
357 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
362 export const buildConnectorsMap
= (
363 connectors
: Record
<string, ConnectorStatus
>,
366 ): Map
<number, ConnectorStatus
> => {
367 const connectorsMap
= new Map
<number, ConnectorStatus
>()
368 if (getMaxNumberOfConnectors(connectors
) > 0) {
369 for (const connector
in connectors
) {
370 const connectorStatus
= connectors
[connector
]
371 const connectorId
= convertToInt(connector
)
372 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
373 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
377 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
383 export const initializeConnectorsMapStatus
= (
384 connectors
: Map
<number, ConnectorStatus
>,
387 for (const connectorId
of connectors
.keys()) {
388 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
390 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
391 connectors.get(connectorId)?.transactionId
395 if (connectorId
=== 0) {
396 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
397 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
398 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 connectors
.get(connectorId
)!.chargingProfiles
= []
402 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
403 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
404 initializeConnectorStatus(connectors
.get(connectorId
)!)
409 export const resetAuthorizeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
410 connectorStatus
.idTagLocalAuthorized
= false
411 connectorStatus
.idTagAuthorized
= false
412 delete connectorStatus
.localAuthorizeIdTag
413 delete connectorStatus
.authorizeIdTag
416 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
417 if (connectorStatus
== null) {
420 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
421 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
.filter(
423 (chargingProfile
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
&&
424 chargingProfile
.transactionId
!= null &&
425 connectorStatus
.transactionId
!= null &&
426 chargingProfile
.transactionId
!== connectorStatus
.transactionId
) ||
427 chargingProfile
.chargingProfilePurpose
!== ChargingProfilePurposeType
.TX_PROFILE
430 resetAuthorizeConnectorStatus(connectorStatus
)
431 connectorStatus
.transactionRemoteStarted
= false
432 connectorStatus
.transactionStarted
= false
433 delete connectorStatus
.transactionStart
434 delete connectorStatus
.transactionId
435 delete connectorStatus
.transactionIdTag
436 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
437 delete connectorStatus
.transactionBeginMeterValue
440 export const prepareConnectorStatus
= (connectorStatus
: ConnectorStatus
): ConnectorStatus
=> {
441 if (connectorStatus
.reservation
!= null) {
442 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
443 connectorStatus
.reservation
.expiryDate
= convertToDate(connectorStatus
.reservation
.expiryDate
)!
445 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
446 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
449 chargingProfile
.chargingProfilePurpose
!== ChargingProfilePurposeType
.TX_PROFILE
451 .map(chargingProfile
=> {
452 chargingProfile
.chargingSchedule
.startSchedule
= convertToDate(
453 chargingProfile
.chargingSchedule
.startSchedule
455 chargingProfile
.validFrom
= convertToDate(chargingProfile
.validFrom
)
456 chargingProfile
.validTo
= convertToDate(chargingProfile
.validTo
)
457 return chargingProfile
460 return connectorStatus
463 export const createBootNotificationRequest
= (
464 stationInfo
: ChargingStationInfo
,
465 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
466 ): BootNotificationRequest
| undefined => {
467 const ocppVersion
= stationInfo
.ocppVersion
468 switch (ocppVersion
) {
469 case OCPPVersion
.VERSION_16
:
471 chargePointModel
: stationInfo
.chargePointModel
,
472 chargePointVendor
: stationInfo
.chargePointVendor
,
473 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
474 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
476 ...(stationInfo
.chargePointSerialNumber
!= null && {
477 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
479 ...(stationInfo
.firmwareVersion
!= null && {
480 firmwareVersion
: stationInfo
.firmwareVersion
482 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
483 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
484 ...(stationInfo
.meterSerialNumber
!= null && {
485 meterSerialNumber
: stationInfo
.meterSerialNumber
487 ...(stationInfo
.meterType
!= null && {
488 meterType
: stationInfo
.meterType
490 } satisfies OCPP16BootNotificationRequest
491 case OCPPVersion
.VERSION_20
:
492 case OCPPVersion
.VERSION_201
:
496 model
: stationInfo
.chargePointModel
,
497 vendorName
: stationInfo
.chargePointVendor
,
498 ...(stationInfo
.firmwareVersion
!= null && {
499 firmwareVersion
: stationInfo
.firmwareVersion
501 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
502 serialNumber
: stationInfo
.chargeBoxSerialNumber
504 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
506 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
507 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
})
511 } satisfies OCPP20BootNotificationRequest
515 export const warnTemplateKeysDeprecation
= (
516 stationTemplate
: ChargingStationTemplate
,
520 const templateKeys
: Array<{ deprecatedKey
: string, key
?: string }> = [
521 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
522 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
523 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
524 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' }
526 for (const templateKey
of templateKeys
) {
527 warnDeprecatedTemplateKey(
529 templateKey
.deprecatedKey
,
532 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
534 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
538 export const stationTemplateToStationInfo
= (
539 stationTemplate
: ChargingStationTemplate
540 ): ChargingStationInfo
=> {
541 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
542 delete stationTemplate
.power
543 delete stationTemplate
.powerUnit
544 delete stationTemplate
.Connectors
545 delete stationTemplate
.Evses
546 delete stationTemplate
.Configuration
547 delete stationTemplate
.AutomaticTransactionGenerator
548 delete stationTemplate
.numberOfConnectors
549 delete stationTemplate
.chargeBoxSerialNumberPrefix
550 delete stationTemplate
.chargePointSerialNumberPrefix
551 delete stationTemplate
.meterSerialNumberPrefix
552 return stationTemplate
as ChargingStationInfo
555 export const createSerialNumber
= (
556 stationTemplate
: ChargingStationTemplate
,
557 stationInfo
: ChargingStationInfo
,
559 randomSerialNumberUpperCase
?: boolean
560 randomSerialNumber
?: boolean
564 ...{ randomSerialNumberUpperCase
: true, randomSerialNumber
: true },
567 const serialNumberSuffix
=
568 params
.randomSerialNumber
=== true
569 ? getRandomSerialNumberSuffix({
570 upperCase
: params
.randomSerialNumberUpperCase
573 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
574 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
575 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
576 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
577 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
578 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
581 export const propagateSerialNumber
= (
582 stationTemplate
: ChargingStationTemplate
| undefined,
583 stationInfoSrc
: ChargingStationInfo
| undefined,
584 stationInfoDst
: ChargingStationInfo
586 if (stationInfoSrc
== null || stationTemplate
== null) {
588 'Missing charging station template or existing configuration to propagate serial number'
591 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
592 stationInfoSrc
.chargePointSerialNumber
!= null
593 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
594 : stationInfoDst
.chargePointSerialNumber
!= null &&
595 delete stationInfoDst
.chargePointSerialNumber
596 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
597 stationInfoSrc
.chargeBoxSerialNumber
!= null
598 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
599 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
600 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
601 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
602 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
605 export const hasFeatureProfile
= (
606 chargingStation
: ChargingStation
,
607 featureProfile
: SupportedFeatureProfiles
608 ): boolean | undefined => {
609 return getConfigurationKey(
611 StandardParametersKey
.SupportedFeatureProfiles
612 )?.value
?.includes(featureProfile
)
615 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
617 switch (stationInfo
.amperageLimitationUnit
) {
618 case AmpereUnits
.DECI_AMPERE
:
621 case AmpereUnits
.CENTI_AMPERE
:
624 case AmpereUnits
.MILLI_AMPERE
:
631 const getChargingStationChargingProfiles
= (
632 chargingStation
: ChargingStation
633 ): ChargingProfile
[] => {
634 return (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? [])
637 chargingProfile
.chargingProfilePurpose
===
638 ChargingProfilePurposeType
.CHARGE_POINT_MAX_PROFILE
640 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
643 export const getChargingStationChargingProfilesLimit
= (
644 chargingStation
: ChargingStation
645 ): number | undefined => {
646 const chargingProfiles
= getChargingStationChargingProfiles(chargingStation
)
647 if (isNotEmptyArray(chargingProfiles
)) {
648 const chargingProfilesLimit
= getChargingProfilesLimit(chargingStation
, 0, chargingProfiles
)
649 if (chargingProfilesLimit
!= null) {
650 const limit
= buildChargingProfilesLimit(chargingStation
, chargingProfilesLimit
)
651 const chargingStationMaximumPower
=
652 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
653 chargingStation
.stationInfo
!.maximumPower
!
654 if (limit
> chargingStationMaximumPower
) {
656 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationChargingProfilesLimit: Charging profile id ${
657 chargingProfilesLimit.chargingProfile.chargingProfileId
658 } limit ${limit} is greater than charging station maximum ${chargingStationMaximumPower}: %j`,
659 chargingProfilesLimit
661 return chargingStationMaximumPower
669 * Gets the connector charging profiles relevant for power limitation shallow cloned
670 * and sorted by priorities
672 * @param chargingStation - Charging station
673 * @param connectorId - Connector id
674 * @returns connector charging profiles array
676 export const getConnectorChargingProfiles
= (
677 chargingStation
: ChargingStation
,
679 ): ChargingProfile
[] => {
680 return (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
684 a
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
&&
685 b
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
689 a
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
&&
690 b
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
694 return b
.stackLevel
- a
.stackLevel
697 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? [])
700 chargingProfile
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
702 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
706 export const getConnectorChargingProfilesLimit
= (
707 chargingStation
: ChargingStation
,
709 ): number | undefined => {
710 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
711 if (isNotEmptyArray(chargingProfiles
)) {
712 const chargingProfilesLimit
= getChargingProfilesLimit(
717 if (chargingProfilesLimit
!= null) {
718 let limit
= buildChargingProfilesLimit(chargingStation
, chargingProfilesLimit
)
719 const connectorMaximumPower
=
720 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
721 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
722 if (limit
> connectorMaximumPower
) {
724 `${chargingStation.logPrefix()} ${moduleName}.getConnectorChargingProfilesLimit: Charging profile id ${
725 chargingProfilesLimit.chargingProfile.chargingProfileId
726 } limit ${limit} is greater than connector ${connectorId} maximum ${connectorMaximumPower}: %j`,
727 chargingProfilesLimit
729 limit
= connectorMaximumPower
736 const buildChargingProfilesLimit
= (
737 chargingStation
: ChargingStation
,
738 chargingProfilesLimit
: ChargingProfilesLimit
740 let { limit
, chargingProfile
} = chargingProfilesLimit
741 switch (chargingStation
.stationInfo
?.currentOutType
) {
744 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
746 : ACElectricUtils
.powerTotal(
747 chargingStation
.getNumberOfPhases(),
748 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
749 chargingStation
.stationInfo
.voltageOut
!,
755 chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
757 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
758 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
763 export const getDefaultVoltageOut
= (
764 currentType
: CurrentType
,
768 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
769 let defaultVoltageOut
: number
770 switch (currentType
) {
772 defaultVoltageOut
= Voltage
.VOLTAGE_230
775 defaultVoltageOut
= Voltage
.VOLTAGE_400
778 logger
.error(`${logPrefix} ${errorMsg}`)
779 throw new BaseError(errorMsg
)
781 return defaultVoltageOut
784 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
785 return stationInfo
.idTagsFile
!= null
786 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
790 export const waitChargingStationEvents
= async (
791 emitter
: EventEmitter
,
792 event
: ChargingStationWorkerMessageEvents
,
794 ): Promise
<number> => {
795 return await new Promise
<number>(resolve
=> {
797 if (eventsToWait
=== 0) {
801 emitter
.on(event
, () => {
803 if (events
=== eventsToWait
) {
810 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
811 let configuredMaxNumberOfConnectors
= 0
812 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
813 const numberOfConnectors
= stationTemplate
.numberOfConnectors
814 configuredMaxNumberOfConnectors
=
815 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
816 } else if (stationTemplate
.numberOfConnectors
!= null) {
817 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
818 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
819 configuredMaxNumberOfConnectors
=
820 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
821 stationTemplate
.Connectors
[0] != null
822 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
823 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
824 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
825 for (const evse
in stationTemplate
.Evses
) {
829 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
830 stationTemplate
.Evses
[evse
].Connectors
834 return configuredMaxNumberOfConnectors
837 const checkConfiguredMaxConnectors
= (
838 configuredMaxConnectors
: number,
842 if (configuredMaxConnectors
<= 0) {
844 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
849 const checkTemplateMaxConnectors
= (
850 templateMaxConnectors
: number,
854 if (templateMaxConnectors
=== 0) {
856 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
858 } else if (templateMaxConnectors
< 0) {
860 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
865 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
866 connectorStatus
.availability
= AvailabilityType
.Operative
867 connectorStatus
.idTagLocalAuthorized
= false
868 connectorStatus
.idTagAuthorized
= false
869 connectorStatus
.transactionRemoteStarted
= false
870 connectorStatus
.transactionStarted
= false
871 connectorStatus
.energyActiveImportRegisterValue
= 0
872 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
873 if (connectorStatus
.chargingProfiles
== null) {
874 connectorStatus
.chargingProfiles
= []
878 const warnDeprecatedTemplateKey
= (
879 template
: ChargingStationTemplate
,
882 templateFile
: string,
885 if (template
[key
as keyof ChargingStationTemplate
] != null) {
886 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
887 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
889 logger
.warn(`${logPrefix} ${logMsg}`)
890 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
894 const convertDeprecatedTemplateKey
= (
895 template
: ChargingStationTemplate
,
896 deprecatedKey
: string,
899 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
901 (template
as unknown
as Record
<string, unknown
>)[key
] =
902 template
[deprecatedKey
as keyof ChargingStationTemplate
]
904 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
905 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
909 interface ChargingProfilesLimit
{
911 chargingProfile
: ChargingProfile
915 * Get the charging profiles limit for a connector
916 * Charging profiles shall already be sorted by priorities
918 * @param chargingStation -
919 * @param connectorId -
920 * @param chargingProfiles -
921 * @returns ChargingProfilesLimit
923 const getChargingProfilesLimit
= (
924 chargingStation
: ChargingStation
,
926 chargingProfiles
: ChargingProfile
[]
927 ): ChargingProfilesLimit
| undefined => {
928 const debugLogMsg
= `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profiles limit found: %j`
929 const currentDate
= new Date()
930 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
931 let previousActiveChargingProfile
: ChargingProfile
| undefined
932 for (const chargingProfile
of chargingProfiles
) {
933 const chargingSchedule
= chargingProfile
.chargingSchedule
934 if (chargingSchedule
.startSchedule
== null) {
936 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
938 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
939 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
941 if (!isDate(chargingSchedule
.startSchedule
)) {
943 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
945 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
946 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
948 if (chargingSchedule
.duration
== null) {
950 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
952 // OCPP specifies that if duration is not defined, it should be infinite
953 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
956 !prepareChargingProfileKind(
960 chargingStation
.logPrefix()
965 if (!canProceedChargingProfile(chargingProfile
, currentDate
, chargingStation
.logPrefix())) {
968 // Check if the charging profile is active
970 isWithinInterval(currentDate
, {
971 start
: chargingSchedule
.startSchedule
,
972 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
975 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
976 const chargingSchedulePeriodCompareFn
= (
977 a
: ChargingSchedulePeriod
,
978 b
: ChargingSchedulePeriod
979 ): number => a
.startPeriod
- b
.startPeriod
981 !isArraySorted
<ChargingSchedulePeriod
>(
982 chargingSchedule
.chargingSchedulePeriod
,
983 chargingSchedulePeriodCompareFn
987 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
989 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
991 // Check if the first schedule period startPeriod property is equal to 0
992 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
994 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
998 // Handle only one schedule period
999 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
1000 const chargingProfilesLimit
: ChargingProfilesLimit
= {
1001 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
1004 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
1005 return chargingProfilesLimit
1007 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
1008 // Search for the right schedule period
1011 chargingSchedulePeriod
1012 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
1013 // Find the right schedule period
1016 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
1020 // Found the schedule period: previous is the correct one
1021 const chargingProfilesLimit
: ChargingProfilesLimit
= {
1022 limit
: previousChargingSchedulePeriod
?.limit
?? chargingSchedulePeriod
.limit
,
1023 chargingProfile
: previousActiveChargingProfile
?? chargingProfile
1025 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
1026 return chargingProfilesLimit
1028 // Handle the last schedule period within the charging profile duration
1030 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
1031 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
1032 differenceInSeconds(
1034 chargingSchedule
.startSchedule
,
1035 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
1037 chargingSchedule
.startSchedule
1038 ) > chargingSchedule
.duration
)
1040 const chargingProfilesLimit
: ChargingProfilesLimit
= {
1041 limit
: chargingSchedulePeriod
.limit
,
1044 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
1045 return chargingProfilesLimit
1047 // Keep a reference to previous charging schedule period
1048 previousChargingSchedulePeriod
= chargingSchedulePeriod
1051 // Keep a reference to previous active charging profile
1052 previousActiveChargingProfile
= chargingProfile
1057 export const prepareChargingProfileKind
= (
1058 connectorStatus
: ConnectorStatus
| undefined,
1059 chargingProfile
: ChargingProfile
,
1060 currentDate
: string | number | Date,
1063 switch (chargingProfile
.chargingProfileKind
) {
1064 case ChargingProfileKindType
.RECURRING
:
1065 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
1068 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
1070 case ChargingProfileKindType
.RELATIVE
:
1071 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
1073 `${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`
1075 delete chargingProfile
.chargingSchedule
.startSchedule
1077 if (connectorStatus
?.transactionStarted
=== true) {
1078 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
1080 // FIXME: handle relative charging profile duration
1086 export const canProceedChargingProfile
= (
1087 chargingProfile
: ChargingProfile
,
1088 currentDate
: string | number | Date,
1092 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1093 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1096 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1097 chargingProfile.chargingProfileId
1098 } is not valid for the current date ${
1099 isDate(currentDate) ? currentDate.toISOString() : currentDate
1105 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1106 chargingProfile
.chargingSchedule
.duration
== null
1109 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1113 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1115 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1119 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1121 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1128 const canProceedRecurringChargingProfile
= (
1129 chargingProfile
: ChargingProfile
,
1133 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1134 chargingProfile
.recurrencyKind
== null
1137 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1142 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1143 chargingProfile
.chargingSchedule
.startSchedule
== null
1146 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1154 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1156 * @param chargingProfile -
1157 * @param currentDate -
1158 * @param logPrefix -
1160 const prepareRecurringChargingProfile
= (
1161 chargingProfile
: ChargingProfile
,
1162 currentDate
: string | number | Date,
1165 const chargingSchedule
= chargingProfile
.chargingSchedule
1166 let recurringIntervalTranslated
= false
1167 let recurringInterval
: Interval
| undefined
1168 switch (chargingProfile
.recurrencyKind
) {
1169 case RecurrencyKindType
.DAILY
:
1170 recurringInterval
= {
1171 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1172 start
: chargingSchedule
.startSchedule
!,
1173 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1174 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1176 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1178 !isWithinInterval(currentDate
, recurringInterval
) &&
1179 isBefore(recurringInterval
.end
, currentDate
)
1181 chargingSchedule
.startSchedule
= addDays(
1182 recurringInterval
.start
,
1183 differenceInDays(currentDate
, recurringInterval
.start
)
1185 recurringInterval
= {
1186 start
: chargingSchedule
.startSchedule
,
1187 end
: addDays(chargingSchedule
.startSchedule
, 1)
1189 recurringIntervalTranslated
= true
1192 case RecurrencyKindType
.WEEKLY
:
1193 recurringInterval
= {
1194 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1195 start
: chargingSchedule
.startSchedule
!,
1196 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1197 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1199 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1201 !isWithinInterval(currentDate
, recurringInterval
) &&
1202 isBefore(recurringInterval
.end
, currentDate
)
1204 chargingSchedule
.startSchedule
= addWeeks(
1205 recurringInterval
.start
,
1206 differenceInWeeks(currentDate
, recurringInterval
.start
)
1208 recurringInterval
= {
1209 start
: chargingSchedule
.startSchedule
,
1210 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1212 recurringIntervalTranslated
= true
1217 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1220 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1221 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1223 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1224 chargingProfile.recurrencyKind
1225 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1226 recurringInterval?.start as Date
1227 ).toISOString()}, ${toDate(
1228 recurringInterval?.end as Date
1229 ).toISOString()}] has not been properly translated to current date ${
1230 isDate(currentDate) ? currentDate.toISOString() : currentDate
1234 return recurringIntervalTranslated
1237 const checkRecurringChargingProfileDuration
= (
1238 chargingProfile
: ChargingProfile
,
1242 if (chargingProfile
.chargingSchedule
.duration
== null) {
1244 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1245 chargingProfile.chargingProfileKind
1246 } charging profile id ${
1247 chargingProfile.chargingProfileId
1248 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1253 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1255 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1258 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1259 chargingProfile.chargingProfileKind
1260 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1261 chargingProfile.chargingSchedule.duration
1262 } is greater than the recurrency time interval duration ${differenceInSeconds(
1267 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1271 const getRandomSerialNumberSuffix
= (params
?: {
1272 randomBytesLength
?: number
1275 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1276 if (params
?.upperCase
=== true) {
1277 return randomSerialNumberSuffix
.toUpperCase()
1279 return randomSerialNumberSuffix