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 const 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 return connectorMaximumPower
736 const buildChargingProfilesLimit
= (
737 chargingStation
: ChargingStation
,
738 chargingProfilesLimit
: ChargingProfilesLimit
740 const errorMsg
= `Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in charging station information, cannot build charging profiles limit`
741 const { limit
, chargingProfile
} = chargingProfilesLimit
742 switch (chargingStation
.stationInfo
?.currentOutType
) {
744 return 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
!,
753 return chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
755 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
756 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
759 `${chargingStation.logPrefix()} ${moduleName}.buildChargingProfilesLimit: ${errorMsg}`
761 throw new BaseError(errorMsg
)
765 export const getDefaultVoltageOut
= (
766 currentType
: CurrentType
,
770 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
771 let defaultVoltageOut
: number
772 switch (currentType
) {
774 defaultVoltageOut
= Voltage
.VOLTAGE_230
777 defaultVoltageOut
= Voltage
.VOLTAGE_400
780 logger
.error(`${logPrefix} ${errorMsg}`)
781 throw new BaseError(errorMsg
)
783 return defaultVoltageOut
786 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
787 return stationInfo
.idTagsFile
!= null
788 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
792 export const waitChargingStationEvents
= async (
793 emitter
: EventEmitter
,
794 event
: ChargingStationWorkerMessageEvents
,
796 ): Promise
<number> => {
797 return await new Promise
<number>(resolve
=> {
799 if (eventsToWait
=== 0) {
803 emitter
.on(event
, () => {
805 if (events
=== eventsToWait
) {
812 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
813 let configuredMaxNumberOfConnectors
= 0
814 if (isNotEmptyArray(stationTemplate
.numberOfConnectors
)) {
815 const numberOfConnectors
= stationTemplate
.numberOfConnectors
816 configuredMaxNumberOfConnectors
=
817 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
818 } else if (stationTemplate
.numberOfConnectors
!= null) {
819 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
820 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
821 configuredMaxNumberOfConnectors
=
822 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
823 stationTemplate
.Connectors
[0] != null
824 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
825 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
826 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
827 for (const evse
in stationTemplate
.Evses
) {
831 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
832 stationTemplate
.Evses
[evse
].Connectors
836 return configuredMaxNumberOfConnectors
839 const checkConfiguredMaxConnectors
= (
840 configuredMaxConnectors
: number,
844 if (configuredMaxConnectors
<= 0) {
846 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
851 const checkTemplateMaxConnectors
= (
852 templateMaxConnectors
: number,
856 if (templateMaxConnectors
=== 0) {
858 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
860 } else if (templateMaxConnectors
< 0) {
862 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
867 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
868 connectorStatus
.availability
= AvailabilityType
.Operative
869 connectorStatus
.idTagLocalAuthorized
= false
870 connectorStatus
.idTagAuthorized
= false
871 connectorStatus
.transactionRemoteStarted
= false
872 connectorStatus
.transactionStarted
= false
873 connectorStatus
.energyActiveImportRegisterValue
= 0
874 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
875 if (connectorStatus
.chargingProfiles
== null) {
876 connectorStatus
.chargingProfiles
= []
880 const warnDeprecatedTemplateKey
= (
881 template
: ChargingStationTemplate
,
884 templateFile
: string,
887 if (template
[key
as keyof ChargingStationTemplate
] != null) {
888 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
889 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
891 logger
.warn(`${logPrefix} ${logMsg}`)
892 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
896 const convertDeprecatedTemplateKey
= (
897 template
: ChargingStationTemplate
,
898 deprecatedKey
: string,
901 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
903 (template
as unknown
as Record
<string, unknown
>)[key
] =
904 template
[deprecatedKey
as keyof ChargingStationTemplate
]
906 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
907 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
911 interface ChargingProfilesLimit
{
913 chargingProfile
: ChargingProfile
917 * Get the charging profiles limit for a connector
918 * Charging profiles shall already be sorted by priorities
920 * @param chargingStation -
921 * @param connectorId -
922 * @param chargingProfiles -
923 * @returns ChargingProfilesLimit
925 const getChargingProfilesLimit
= (
926 chargingStation
: ChargingStation
,
928 chargingProfiles
: ChargingProfile
[]
929 ): ChargingProfilesLimit
| undefined => {
930 const debugLogMsg
= `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profiles limit found: %j`
931 const currentDate
= new Date()
932 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
933 let previousActiveChargingProfile
: ChargingProfile
| undefined
934 for (const chargingProfile
of chargingProfiles
) {
935 const chargingSchedule
= chargingProfile
.chargingSchedule
936 if (chargingSchedule
.startSchedule
== null) {
938 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
940 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
941 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
943 if (!isDate(chargingSchedule
.startSchedule
)) {
945 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
947 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
948 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
950 if (chargingSchedule
.duration
== null) {
952 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
954 // OCPP specifies that if duration is not defined, it should be infinite
955 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
958 !prepareChargingProfileKind(
962 chargingStation
.logPrefix()
967 if (!canProceedChargingProfile(chargingProfile
, currentDate
, chargingStation
.logPrefix())) {
970 // Check if the charging profile is active
972 isWithinInterval(currentDate
, {
973 start
: chargingSchedule
.startSchedule
,
974 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
)
977 if (isNotEmptyArray(chargingSchedule
.chargingSchedulePeriod
)) {
978 const chargingSchedulePeriodCompareFn
= (
979 a
: ChargingSchedulePeriod
,
980 b
: ChargingSchedulePeriod
981 ): number => a
.startPeriod
- b
.startPeriod
983 !isArraySorted
<ChargingSchedulePeriod
>(
984 chargingSchedule
.chargingSchedulePeriod
,
985 chargingSchedulePeriodCompareFn
989 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
991 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
993 // Check if the first schedule period startPeriod property is equal to 0
994 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
996 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
1000 // Handle only one schedule period
1001 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
1002 const chargingProfilesLimit
: ChargingProfilesLimit
= {
1003 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
1006 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
1007 return chargingProfilesLimit
1009 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
1010 // Search for the right schedule period
1013 chargingSchedulePeriod
1014 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
1015 // Find the right schedule period
1018 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
1022 // Found the schedule period: previous is the correct one
1023 const chargingProfilesLimit
: ChargingProfilesLimit
= {
1024 limit
: previousChargingSchedulePeriod
?.limit
?? chargingSchedulePeriod
.limit
,
1025 chargingProfile
: previousActiveChargingProfile
?? chargingProfile
1027 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
1028 return chargingProfilesLimit
1030 // Handle the last schedule period within the charging profile duration
1032 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
1033 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
1034 differenceInSeconds(
1036 chargingSchedule
.startSchedule
,
1037 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
1039 chargingSchedule
.startSchedule
1040 ) > chargingSchedule
.duration
)
1042 const chargingProfilesLimit
: ChargingProfilesLimit
= {
1043 limit
: chargingSchedulePeriod
.limit
,
1046 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
1047 return chargingProfilesLimit
1049 // Keep a reference to previous charging schedule period
1050 previousChargingSchedulePeriod
= chargingSchedulePeriod
1053 // Keep a reference to previous active charging profile
1054 previousActiveChargingProfile
= chargingProfile
1059 export const prepareChargingProfileKind
= (
1060 connectorStatus
: ConnectorStatus
| undefined,
1061 chargingProfile
: ChargingProfile
,
1062 currentDate
: string | number | Date,
1065 switch (chargingProfile
.chargingProfileKind
) {
1066 case ChargingProfileKindType
.RECURRING
:
1067 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
1070 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
1072 case ChargingProfileKindType
.RELATIVE
:
1073 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
1075 `${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`
1077 delete chargingProfile
.chargingSchedule
.startSchedule
1079 if (connectorStatus
?.transactionStarted
=== true) {
1080 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
1082 // FIXME: handle relative charging profile duration
1088 export const canProceedChargingProfile
= (
1089 chargingProfile
: ChargingProfile
,
1090 currentDate
: string | number | Date,
1094 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1095 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1098 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1099 chargingProfile.chargingProfileId
1100 } is not valid for the current date ${
1101 isDate(currentDate) ? currentDate.toISOString() : currentDate
1107 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1108 chargingProfile
.chargingSchedule
.duration
== null
1111 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1115 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1117 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1121 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1123 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1130 const canProceedRecurringChargingProfile
= (
1131 chargingProfile
: ChargingProfile
,
1135 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1136 chargingProfile
.recurrencyKind
== null
1139 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1144 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1145 chargingProfile
.chargingSchedule
.startSchedule
== null
1148 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1156 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1158 * @param chargingProfile -
1159 * @param currentDate -
1160 * @param logPrefix -
1162 const prepareRecurringChargingProfile
= (
1163 chargingProfile
: ChargingProfile
,
1164 currentDate
: string | number | Date,
1167 const chargingSchedule
= chargingProfile
.chargingSchedule
1168 let recurringIntervalTranslated
= false
1169 let recurringInterval
: Interval
| undefined
1170 switch (chargingProfile
.recurrencyKind
) {
1171 case RecurrencyKindType
.DAILY
:
1172 recurringInterval
= {
1173 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1174 start
: chargingSchedule
.startSchedule
!,
1175 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1176 end
: addDays(chargingSchedule
.startSchedule
!, 1)
1178 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1180 !isWithinInterval(currentDate
, recurringInterval
) &&
1181 isBefore(recurringInterval
.end
, currentDate
)
1183 chargingSchedule
.startSchedule
= addDays(
1184 recurringInterval
.start
,
1185 differenceInDays(currentDate
, recurringInterval
.start
)
1187 recurringInterval
= {
1188 start
: chargingSchedule
.startSchedule
,
1189 end
: addDays(chargingSchedule
.startSchedule
, 1)
1191 recurringIntervalTranslated
= true
1194 case RecurrencyKindType
.WEEKLY
:
1195 recurringInterval
= {
1196 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1197 start
: chargingSchedule
.startSchedule
!,
1198 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1199 end
: addWeeks(chargingSchedule
.startSchedule
!, 1)
1201 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1203 !isWithinInterval(currentDate
, recurringInterval
) &&
1204 isBefore(recurringInterval
.end
, currentDate
)
1206 chargingSchedule
.startSchedule
= addWeeks(
1207 recurringInterval
.start
,
1208 differenceInWeeks(currentDate
, recurringInterval
.start
)
1210 recurringInterval
= {
1211 start
: chargingSchedule
.startSchedule
,
1212 end
: addWeeks(chargingSchedule
.startSchedule
, 1)
1214 recurringIntervalTranslated
= true
1219 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1222 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1223 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1225 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1226 chargingProfile.recurrencyKind
1227 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1228 recurringInterval?.start as Date
1229 ).toISOString()}, ${toDate(
1230 recurringInterval?.end as Date
1231 ).toISOString()}] has not been properly translated to current date ${
1232 isDate(currentDate) ? currentDate.toISOString() : currentDate
1236 return recurringIntervalTranslated
1239 const checkRecurringChargingProfileDuration
= (
1240 chargingProfile
: ChargingProfile
,
1244 if (chargingProfile
.chargingSchedule
.duration
== null) {
1246 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1247 chargingProfile.chargingProfileKind
1248 } charging profile id ${
1249 chargingProfile.chargingProfileId
1250 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1255 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1257 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1260 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1261 chargingProfile.chargingProfileKind
1262 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1263 chargingProfile.chargingSchedule.duration
1264 } is greater than the recurrency time interval duration ${differenceInSeconds(
1269 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1273 const getRandomSerialNumberSuffix
= (params
?: {
1274 randomBytesLength
?: number
1277 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1278 if (params
?.upperCase
=== true) {
1279 return randomSerialNumberSuffix
.toUpperCase()
1281 return randomSerialNumberSuffix