1 import type { EventEmitter
} from
'node:events'
3 import chalk from
'chalk'
19 import { maxTime
} from
'date-fns/constants'
20 import { hash
, randomBytes
} from
'node:crypto'
21 import { basename
, dirname
, isAbsolute
, join
, parse
, relative
, resolve
} from
'node:path'
22 import { env
} from
'node:process'
23 import { fileURLToPath
} from
'node:url'
25 import type { ChargingStation
} from
'./ChargingStation.js'
27 import { BaseError
} from
'../exception/index.js'
31 type BootNotificationRequest
,
34 ChargingProfileKindType
,
35 ChargingProfilePurposeType
,
37 type ChargingSchedulePeriod
,
38 type ChargingStationConfiguration
,
39 type ChargingStationInfo
,
40 type ChargingStationOptions
,
41 type ChargingStationTemplate
,
42 type ChargingStationWorkerMessageEvents
,
43 ConnectorPhaseRotation
,
48 type OCPP16BootNotificationRequest
,
49 type OCPP20BootNotificationRequest
,
53 ReservationTerminationReason
,
54 StandardParametersKey
,
55 type SupportedFeatureProfiles
,
57 } from
'../types/index.js'
72 } from
'../utils/index.js'
73 import { getConfigurationKey
} from
'./ConfigurationKeyUtils.js'
75 const moduleName
= 'Helpers'
77 export const buildTemplateName
= (templateFile
: string): string => {
78 if (isAbsolute(templateFile
)) {
79 templateFile
= relative(
80 resolve(join(dirname(fileURLToPath(import.meta
.url
)), 'assets', 'station-templates')),
84 const templateFileParsedPath
= parse(templateFile
)
85 return join(templateFileParsedPath
.dir
, templateFileParsedPath
.name
)
88 export const getChargingStationId
= (
90 stationTemplate
: ChargingStationTemplate
| undefined
92 if (stationTemplate
== null) {
93 return "Unknown 'chargingStationId'"
95 // In case of multiple instances: add instance index to charging station id
96 const instanceIndex
= env
.CF_INSTANCE_INDEX
?? 0
97 const idSuffix
= stationTemplate
.nameSuffix
?? ''
98 const idStr
= `000000000${index.toString()}`
99 return stationTemplate
.fixedName
=== true
100 ? stationTemplate
.baseName
101 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
106 export const hasReservationExpired
= (reservation
: Reservation
): boolean => {
107 return isPast(reservation
.expiryDate
)
110 export const removeExpiredReservations
= async (
111 chargingStation
: ChargingStation
112 ): Promise
<void> => {
113 if (chargingStation
.hasEvses
) {
114 for (const evseStatus
of chargingStation
.evses
.values()) {
115 for (const connectorStatus
of evseStatus
.connectors
.values()) {
117 connectorStatus
.reservation
!= null &&
118 hasReservationExpired(connectorStatus
.reservation
)
120 await chargingStation
.removeReservation(
121 connectorStatus
.reservation
,
122 ReservationTerminationReason
.EXPIRED
128 for (const connectorStatus
of chargingStation
.connectors
.values()) {
130 connectorStatus
.reservation
!= null &&
131 hasReservationExpired(connectorStatus
.reservation
)
133 await chargingStation
.removeReservation(
134 connectorStatus
.reservation
,
135 ReservationTerminationReason
.EXPIRED
142 export const getNumberOfReservableConnectors
= (
143 connectors
: Map
<number, ConnectorStatus
>
145 let numberOfReservableConnectors
= 0
146 for (const [connectorId
, connectorStatus
] of connectors
) {
147 if (connectorId
=== 0) {
150 if (connectorStatus
.status === ConnectorStatusEnum
.Available
) {
151 ++numberOfReservableConnectors
154 return numberOfReservableConnectors
157 export const getHashId
= (index
: number, stationTemplate
: ChargingStationTemplate
): string => {
158 const chargingStationInfo
= {
159 chargePointModel
: stationTemplate
.chargePointModel
,
160 chargePointVendor
: stationTemplate
.chargePointVendor
,
161 ...(stationTemplate
.chargeBoxSerialNumberPrefix
!= null && {
162 chargeBoxSerialNumber
: stationTemplate
.chargeBoxSerialNumberPrefix
,
164 ...(stationTemplate
.chargePointSerialNumberPrefix
!= null && {
165 chargePointSerialNumber
: stationTemplate
.chargePointSerialNumberPrefix
,
167 ...(stationTemplate
.meterSerialNumberPrefix
!= null && {
168 meterSerialNumber
: stationTemplate
.meterSerialNumberPrefix
,
170 ...(stationTemplate
.meterType
!= null && {
171 meterType
: stationTemplate
.meterType
,
175 Constants
.DEFAULT_HASH_ALGORITHM
,
176 `${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`,
181 export const validateStationInfo
= (chargingStation
: ChargingStation
): void => {
182 if (chargingStation
.stationInfo
== null || isEmpty(chargingStation
.stationInfo
)) {
183 throw new BaseError('Missing charging station information')
186 chargingStation
.stationInfo
.chargingStationId
== null ||
187 isEmpty(chargingStation
.stationInfo
.chargingStationId
.trim())
189 throw new BaseError('Missing chargingStationId in stationInfo properties')
191 const chargingStationId
= chargingStation
.stationInfo
.chargingStationId
193 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
194 chargingStation
.stationInfo
.hashId
== null ||
195 isEmpty(chargingStation
.stationInfo
.hashId
.trim())
197 throw new BaseError(`${chargingStationId}: Missing hashId in stationInfo properties`)
199 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
200 if (chargingStation
.stationInfo
.templateIndex
== null) {
201 throw new BaseError(`${chargingStationId}: Missing templateIndex in stationInfo properties`)
203 if (chargingStation
.stationInfo
.templateIndex
<= 0) {
205 `${chargingStationId}: Invalid templateIndex value in stationInfo properties`
209 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
210 chargingStation
.stationInfo
.templateName
== null ||
211 isEmpty(chargingStation
.stationInfo
.templateName
.trim())
213 throw new BaseError(`${chargingStationId}: Missing templateName in stationInfo properties`)
215 if (chargingStation
.stationInfo
.maximumPower
== null) {
216 throw new BaseError(`${chargingStationId}: Missing maximumPower in stationInfo properties`)
218 if (chargingStation
.stationInfo
.maximumPower
<= 0) {
219 throw new RangeError(
220 `${chargingStationId}: Invalid maximumPower value in stationInfo properties`
223 if (chargingStation
.stationInfo
.maximumAmperage
== null) {
224 throw new BaseError(`${chargingStationId}: Missing maximumAmperage in stationInfo properties`)
226 if (chargingStation
.stationInfo
.maximumAmperage
<= 0) {
227 throw new RangeError(
228 `${chargingStationId}: Invalid maximumAmperage value in stationInfo properties`
231 switch (chargingStation
.stationInfo
.ocppVersion
) {
232 case OCPPVersion
.VERSION_20
:
233 case OCPPVersion
.VERSION_201
:
234 if (isEmpty(chargingStation
.evses
)) {
236 `${chargingStationId}: OCPP 2.0 or superior requires at least one EVSE defined in the charging station template/configuration`
242 export const checkChargingStationState
= (
243 chargingStation
: ChargingStation
,
246 if (!chargingStation
.started
&& !chargingStation
.starting
) {
247 logger
.warn(`${logPrefix} charging station is stopped, cannot proceed`)
253 export const getPhaseRotationValue
= (
255 numberOfPhases
: number
256 ): string | undefined => {
258 if (connectorId
=== 0 && numberOfPhases
=== 0) {
259 return `${connectorId.toString()}.${ConnectorPhaseRotation.RST}`
260 } else if (connectorId
> 0 && numberOfPhases
=== 0) {
261 return `${connectorId.toString()}.${ConnectorPhaseRotation.NotApplicable}`
263 } else if (connectorId
>= 0 && numberOfPhases
=== 1) {
264 return `${connectorId.toString()}.${ConnectorPhaseRotation.NotApplicable}`
265 } else if (connectorId
>= 0 && numberOfPhases
=== 3) {
266 return `${connectorId.toString()}.${ConnectorPhaseRotation.RST}`
270 export const getMaxNumberOfEvses
= (evses
: Record
<string, EvseTemplate
> | undefined): number => {
274 return Object.keys(evses
).length
277 const getMaxNumberOfConnectors
= (
278 connectors
: Record
<string, ConnectorStatus
> | undefined
280 if (connectors
== null) {
283 return Object.keys(connectors
).length
286 export const getBootConnectorStatus
= (
287 chargingStation
: ChargingStation
,
289 connectorStatus
: ConnectorStatus
290 ): ConnectorStatusEnum
=> {
291 let connectorBootStatus
: ConnectorStatusEnum
293 connectorStatus
.status == null &&
294 (!chargingStation
.isChargingStationAvailable() ||
295 !chargingStation
.isConnectorAvailable(connectorId
))
297 connectorBootStatus
= ConnectorStatusEnum
.Unavailable
298 } else if (connectorStatus
.status == null && connectorStatus
.bootStatus
!= null) {
299 // Set boot status in template at startup
300 connectorBootStatus
= connectorStatus
.bootStatus
301 } else if (connectorStatus
.status != null) {
302 // Set previous status at startup
303 connectorBootStatus
= connectorStatus
.status
305 // Set default status
306 connectorBootStatus
= ConnectorStatusEnum
.Available
308 return connectorBootStatus
311 export const checkTemplate
= (
312 stationTemplate
: ChargingStationTemplate
| undefined,
316 if (stationTemplate
== null) {
317 const errorMsg
= `Failed to read charging station template file ${templateFile}`
318 logger
.error(`${logPrefix} ${errorMsg}`)
319 throw new BaseError(errorMsg
)
321 if (isEmpty(stationTemplate
)) {
322 const errorMsg
= `Empty charging station information from template file ${templateFile}`
323 logger
.error(`${logPrefix} ${errorMsg}`)
324 throw new BaseError(errorMsg
)
326 if (stationTemplate
.idTagsFile
== null || isEmpty(stationTemplate
.idTagsFile
)) {
328 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
333 export const checkConfiguration
= (
334 stationConfiguration
: ChargingStationConfiguration
| undefined,
336 configurationFile
: string
338 if (stationConfiguration
== null) {
339 const errorMsg
= `Failed to read charging station configuration file ${configurationFile}`
340 logger
.error(`${logPrefix} ${errorMsg}`)
341 throw new BaseError(errorMsg
)
343 if (isEmpty(stationConfiguration
)) {
344 const errorMsg
= `Empty charging station configuration from file ${configurationFile}`
345 logger
.error(`${logPrefix} ${errorMsg}`)
346 throw new BaseError(errorMsg
)
350 export const checkConnectorsConfiguration
= (
351 stationTemplate
: ChargingStationTemplate
,
355 configuredMaxConnectors
: number
356 templateMaxAvailableConnectors
: number
357 templateMaxConnectors
: number
359 const configuredMaxConnectors
= getConfiguredMaxNumberOfConnectors(stationTemplate
)
360 checkConfiguredMaxConnectors(configuredMaxConnectors
, logPrefix
, templateFile
)
361 const templateMaxConnectors
= getMaxNumberOfConnectors(stationTemplate
.Connectors
)
362 checkTemplateMaxConnectors(templateMaxConnectors
, logPrefix
, templateFile
)
363 const templateMaxAvailableConnectors
=
364 stationTemplate
.Connectors
?.[0] != null ? templateMaxConnectors
- 1 : templateMaxConnectors
366 configuredMaxConnectors
> templateMaxAvailableConnectors
&&
367 stationTemplate
.randomConnectors
!== true
370 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
372 stationTemplate
.randomConnectors
= true
375 configuredMaxConnectors
,
376 templateMaxAvailableConnectors
,
377 templateMaxConnectors
,
381 export const checkStationInfoConnectorStatus
= (
383 connectorStatus
: ConnectorStatus
,
387 if (connectorStatus
.status != null) {
389 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId.toString()} status configuration defined, undefine it`
391 delete connectorStatus
.status
395 export const setChargingStationOptions
= (
396 stationInfo
: ChargingStationInfo
,
397 options
?: ChargingStationOptions
398 ): ChargingStationInfo
=> {
399 if (options
?.supervisionUrls
!= null) {
400 stationInfo
.supervisionUrls
= options
.supervisionUrls
402 if (options
?.persistentConfiguration
!= null) {
403 stationInfo
.stationInfoPersistentConfiguration
= options
.persistentConfiguration
404 stationInfo
.ocppPersistentConfiguration
= options
.persistentConfiguration
405 stationInfo
.automaticTransactionGeneratorPersistentConfiguration
=
406 options
.persistentConfiguration
408 if (options
?.autoStart
!= null) {
409 stationInfo
.autoStart
= options
.autoStart
411 if (options
?.autoRegister
!= null) {
412 stationInfo
.autoRegister
= options
.autoRegister
414 if (options
?.enableStatistics
!= null) {
415 stationInfo
.enableStatistics
= options
.enableStatistics
417 if (options
?.ocppStrictCompliance
!= null) {
418 stationInfo
.ocppStrictCompliance
= options
.ocppStrictCompliance
420 if (options
?.stopTransactionsOnStopped
!= null) {
421 stationInfo
.stopTransactionsOnStopped
= options
.stopTransactionsOnStopped
426 export const buildConnectorsMap
= (
427 connectors
: Record
<string, ConnectorStatus
>,
430 ): Map
<number, ConnectorStatus
> => {
431 const connectorsMap
= new Map
<number, ConnectorStatus
>()
432 if (getMaxNumberOfConnectors(connectors
) > 0) {
433 for (const connector
in connectors
) {
434 const connectorStatus
= connectors
[connector
]
435 const connectorId
= convertToInt(connector
)
436 checkStationInfoConnectorStatus(connectorId
, connectorStatus
, logPrefix
, templateFile
)
437 connectorsMap
.set(connectorId
, clone
<ConnectorStatus
>(connectorStatus
))
441 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
447 export const initializeConnectorsMapStatus
= (
448 connectors
: Map
<number, ConnectorStatus
>,
451 for (const connectorId
of connectors
.keys()) {
452 if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
=== true) {
454 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
455 `${logPrefix} Connector id ${connectorId.toString()} at initialization has a transaction started with id ${connectors
457 ?.transactionId?.toString()}`
460 if (connectorId
=== 0) {
461 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
462 connectors
.get(connectorId
)!.availability
= AvailabilityType
.Operative
463 if (connectors
.get(connectorId
)?.chargingProfiles
== null) {
464 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
465 connectors
.get(connectorId
)!.chargingProfiles
= []
467 } else if (connectorId
> 0 && connectors
.get(connectorId
)?.transactionStarted
== null) {
468 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
469 initializeConnectorStatus(connectors
.get(connectorId
)!)
474 export const resetAuthorizeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
475 connectorStatus
.idTagLocalAuthorized
= false
476 connectorStatus
.idTagAuthorized
= false
477 delete connectorStatus
.localAuthorizeIdTag
478 delete connectorStatus
.authorizeIdTag
481 export const resetConnectorStatus
= (connectorStatus
: ConnectorStatus
| undefined): void => {
482 if (connectorStatus
== null) {
485 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
486 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
.filter(
488 (chargingProfile
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
&&
489 chargingProfile
.transactionId
!= null &&
490 connectorStatus
.transactionId
!= null &&
491 chargingProfile
.transactionId
!== connectorStatus
.transactionId
) ||
492 chargingProfile
.chargingProfilePurpose
!== ChargingProfilePurposeType
.TX_PROFILE
495 resetAuthorizeConnectorStatus(connectorStatus
)
496 connectorStatus
.transactionRemoteStarted
= false
497 connectorStatus
.transactionStarted
= false
498 delete connectorStatus
.transactionStart
499 delete connectorStatus
.transactionId
500 delete connectorStatus
.transactionIdTag
501 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
502 delete connectorStatus
.transactionBeginMeterValue
505 export const prepareConnectorStatus
= (connectorStatus
: ConnectorStatus
): ConnectorStatus
=> {
506 if (connectorStatus
.reservation
!= null) {
507 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
508 connectorStatus
.reservation
.expiryDate
= convertToDate(connectorStatus
.reservation
.expiryDate
)!
510 if (isNotEmptyArray(connectorStatus
.chargingProfiles
)) {
511 connectorStatus
.chargingProfiles
= connectorStatus
.chargingProfiles
514 chargingProfile
.chargingProfilePurpose
!== ChargingProfilePurposeType
.TX_PROFILE
516 .map(chargingProfile
=> {
517 chargingProfile
.chargingSchedule
.startSchedule
= convertToDate(
518 chargingProfile
.chargingSchedule
.startSchedule
520 chargingProfile
.validFrom
= convertToDate(chargingProfile
.validFrom
)
521 chargingProfile
.validTo
= convertToDate(chargingProfile
.validTo
)
522 return chargingProfile
525 return connectorStatus
528 export const createBootNotificationRequest
= (
529 stationInfo
: ChargingStationInfo
,
530 bootReason
: BootReasonEnumType
= BootReasonEnumType
.PowerUp
531 ): BootNotificationRequest
| undefined => {
532 const ocppVersion
= stationInfo
.ocppVersion
533 switch (ocppVersion
) {
534 case OCPPVersion
.VERSION_16
:
536 chargePointModel
: stationInfo
.chargePointModel
,
537 chargePointVendor
: stationInfo
.chargePointVendor
,
538 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
539 chargeBoxSerialNumber
: stationInfo
.chargeBoxSerialNumber
,
541 ...(stationInfo
.chargePointSerialNumber
!= null && {
542 chargePointSerialNumber
: stationInfo
.chargePointSerialNumber
,
544 ...(stationInfo
.firmwareVersion
!= null && {
545 firmwareVersion
: stationInfo
.firmwareVersion
,
547 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
548 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
549 ...(stationInfo
.meterSerialNumber
!= null && {
550 meterSerialNumber
: stationInfo
.meterSerialNumber
,
552 ...(stationInfo
.meterType
!= null && {
553 meterType
: stationInfo
.meterType
,
555 } satisfies OCPP16BootNotificationRequest
556 case OCPPVersion
.VERSION_20
:
557 case OCPPVersion
.VERSION_201
:
560 model
: stationInfo
.chargePointModel
,
561 vendorName
: stationInfo
.chargePointVendor
,
562 ...(stationInfo
.firmwareVersion
!= null && {
563 firmwareVersion
: stationInfo
.firmwareVersion
,
565 ...(stationInfo
.chargeBoxSerialNumber
!= null && {
566 serialNumber
: stationInfo
.chargeBoxSerialNumber
,
568 ...((stationInfo
.iccid
!= null || stationInfo
.imsi
!= null) && {
570 ...(stationInfo
.iccid
!= null && { iccid
: stationInfo
.iccid
}),
571 ...(stationInfo
.imsi
!= null && { imsi
: stationInfo
.imsi
}),
576 } satisfies OCPP20BootNotificationRequest
580 export const warnTemplateKeysDeprecation
= (
581 stationTemplate
: ChargingStationTemplate
,
585 const templateKeys
: { deprecatedKey
: string; key
?: string }[] = [
586 { deprecatedKey
: 'supervisionUrl', key
: 'supervisionUrls' },
587 { deprecatedKey
: 'authorizationFile', key
: 'idTagsFile' },
588 { deprecatedKey
: 'payloadSchemaValidation', key
: 'ocppStrictCompliance' },
589 { deprecatedKey
: 'mustAuthorizeAtRemoteStart', key
: 'remoteAuthorization' },
591 for (const templateKey
of templateKeys
) {
592 warnDeprecatedTemplateKey(
594 templateKey
.deprecatedKey
,
597 templateKey
.key
!= null ? `Use '${templateKey.key}' instead` : undefined
599 convertDeprecatedTemplateKey(stationTemplate
, templateKey
.deprecatedKey
, templateKey
.key
)
603 export const stationTemplateToStationInfo
= (
604 stationTemplate
: ChargingStationTemplate
605 ): ChargingStationInfo
=> {
606 stationTemplate
= clone
<ChargingStationTemplate
>(stationTemplate
)
607 delete stationTemplate
.power
608 delete stationTemplate
.powerUnit
609 delete stationTemplate
.Connectors
610 delete stationTemplate
.Evses
611 delete stationTemplate
.Configuration
612 delete stationTemplate
.AutomaticTransactionGenerator
613 delete stationTemplate
.numberOfConnectors
614 delete stationTemplate
.chargeBoxSerialNumberPrefix
615 delete stationTemplate
.chargePointSerialNumberPrefix
616 delete stationTemplate
.meterSerialNumberPrefix
617 return stationTemplate
as ChargingStationInfo
620 export const createSerialNumber
= (
621 stationTemplate
: ChargingStationTemplate
,
622 stationInfo
: ChargingStationInfo
,
624 randomSerialNumber
?: boolean
625 randomSerialNumberUpperCase
?: boolean
629 ...{ randomSerialNumber
: true, randomSerialNumberUpperCase
: true },
632 const serialNumberSuffix
= params
.randomSerialNumber
633 ? getRandomSerialNumberSuffix({
634 upperCase
: params
.randomSerialNumberUpperCase
,
637 isNotEmptyString(stationTemplate
.chargePointSerialNumberPrefix
) &&
638 (stationInfo
.chargePointSerialNumber
= `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
639 isNotEmptyString(stationTemplate
.chargeBoxSerialNumberPrefix
) &&
640 (stationInfo
.chargeBoxSerialNumber
= `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
641 isNotEmptyString(stationTemplate
.meterSerialNumberPrefix
) &&
642 (stationInfo
.meterSerialNumber
= `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
645 export const propagateSerialNumber
= (
646 stationTemplate
: ChargingStationTemplate
| undefined,
647 stationInfoSrc
: ChargingStationInfo
| undefined,
648 stationInfoDst
: ChargingStationInfo
650 if (stationInfoSrc
== null || stationTemplate
== null) {
652 'Missing charging station template or existing configuration to propagate serial number'
655 stationTemplate
.chargePointSerialNumberPrefix
!= null &&
656 stationInfoSrc
.chargePointSerialNumber
!= null
657 ? (stationInfoDst
.chargePointSerialNumber
= stationInfoSrc
.chargePointSerialNumber
)
658 : stationInfoDst
.chargePointSerialNumber
!= null &&
659 delete stationInfoDst
.chargePointSerialNumber
660 stationTemplate
.chargeBoxSerialNumberPrefix
!= null &&
661 stationInfoSrc
.chargeBoxSerialNumber
!= null
662 ? (stationInfoDst
.chargeBoxSerialNumber
= stationInfoSrc
.chargeBoxSerialNumber
)
663 : stationInfoDst
.chargeBoxSerialNumber
!= null && delete stationInfoDst
.chargeBoxSerialNumber
664 stationTemplate
.meterSerialNumberPrefix
!= null && stationInfoSrc
.meterSerialNumber
!= null
665 ? (stationInfoDst
.meterSerialNumber
= stationInfoSrc
.meterSerialNumber
)
666 : stationInfoDst
.meterSerialNumber
!= null && delete stationInfoDst
.meterSerialNumber
669 export const hasFeatureProfile
= (
670 chargingStation
: ChargingStation
,
671 featureProfile
: SupportedFeatureProfiles
672 ): boolean | undefined => {
673 return getConfigurationKey(
675 StandardParametersKey
.SupportedFeatureProfiles
676 )?.value
?.includes(featureProfile
)
679 export const getAmperageLimitationUnitDivider
= (stationInfo
: ChargingStationInfo
): number => {
681 switch (stationInfo
.amperageLimitationUnit
) {
682 case AmpereUnits
.CENTI_AMPERE
:
685 case AmpereUnits
.DECI_AMPERE
:
688 case AmpereUnits
.MILLI_AMPERE
:
695 const getChargingStationChargingProfiles
= (
696 chargingStation
: ChargingStation
697 ): ChargingProfile
[] => {
698 return (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? [])
701 chargingProfile
.chargingProfilePurpose
===
702 ChargingProfilePurposeType
.CHARGE_POINT_MAX_PROFILE
704 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
707 export const getChargingStationChargingProfilesLimit
= (
708 chargingStation
: ChargingStation
709 ): number | undefined => {
710 const chargingProfiles
= getChargingStationChargingProfiles(chargingStation
)
711 if (isNotEmptyArray(chargingProfiles
)) {
712 const chargingProfilesLimit
= getChargingProfilesLimit(chargingStation
, 0, chargingProfiles
)
713 if (chargingProfilesLimit
!= null) {
714 const limit
= buildChargingProfilesLimit(chargingStation
, chargingProfilesLimit
)
715 const chargingStationMaximumPower
=
716 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
717 chargingStation
.stationInfo
!.maximumPower
!
718 if (limit
> chargingStationMaximumPower
) {
720 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationChargingProfilesLimit: Charging profile id ${chargingProfilesLimit.chargingProfile.chargingProfileId.toString()} limit ${limit.toString()} is greater than charging station maximum ${chargingStationMaximumPower.toString()}: %j`,
721 chargingProfilesLimit
723 return chargingStationMaximumPower
731 * Gets the connector charging profiles relevant for power limitation shallow cloned
732 * and sorted by priorities
733 * @param chargingStation - Charging station
734 * @param connectorId - Connector id
735 * @returns connector charging profiles array
737 export const getConnectorChargingProfiles
= (
738 chargingStation
: ChargingStation
,
740 ): ChargingProfile
[] => {
741 return (chargingStation
.getConnectorStatus(connectorId
)?.chargingProfiles
?? [])
745 a
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
&&
746 b
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
750 a
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
&&
751 b
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_PROFILE
755 return b
.stackLevel
- a
.stackLevel
758 (chargingStation
.getConnectorStatus(0)?.chargingProfiles
?? [])
761 chargingProfile
.chargingProfilePurpose
=== ChargingProfilePurposeType
.TX_DEFAULT_PROFILE
763 .sort((a
, b
) => b
.stackLevel
- a
.stackLevel
)
767 export const getConnectorChargingProfilesLimit
= (
768 chargingStation
: ChargingStation
,
770 ): number | undefined => {
771 const chargingProfiles
= getConnectorChargingProfiles(chargingStation
, connectorId
)
772 if (isNotEmptyArray(chargingProfiles
)) {
773 const chargingProfilesLimit
= getChargingProfilesLimit(
778 if (chargingProfilesLimit
!= null) {
779 const limit
= buildChargingProfilesLimit(chargingStation
, chargingProfilesLimit
)
780 const connectorMaximumPower
=
781 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
782 chargingStation
.stationInfo
!.maximumPower
! / chargingStation
.powerDivider
!
783 if (limit
> connectorMaximumPower
) {
785 `${chargingStation.logPrefix()} ${moduleName}.getConnectorChargingProfilesLimit: Charging profile id ${chargingProfilesLimit.chargingProfile.chargingProfileId.toString()} limit ${limit.toString()} is greater than connector ${connectorId.toString()} maximum ${connectorMaximumPower.toString()}: %j`,
786 chargingProfilesLimit
788 return connectorMaximumPower
795 const buildChargingProfilesLimit
= (
796 chargingStation
: ChargingStation
,
797 chargingProfilesLimit
: ChargingProfilesLimit
799 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
800 const errorMsg
= `Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in charging station information, cannot build charging profiles limit`
801 const { chargingProfile
, limit
} = chargingProfilesLimit
802 switch (chargingStation
.stationInfo
?.currentOutType
) {
804 return chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
806 : ACElectricUtils
.powerTotal(
807 chargingStation
.getNumberOfPhases(),
808 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
809 chargingStation
.stationInfo
.voltageOut
!,
813 return chargingProfile
.chargingSchedule
.chargingRateUnit
=== ChargingRateUnitType
.WATT
815 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
816 DCElectricUtils
.power(chargingStation
.stationInfo
.voltageOut
!, limit
)
819 `${chargingStation.logPrefix()} ${moduleName}.buildChargingProfilesLimit: ${errorMsg}`
821 throw new BaseError(errorMsg
)
825 export const getDefaultVoltageOut
= (
826 currentType
: CurrentType
,
830 const errorMsg
= `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
831 let defaultVoltageOut
: number
832 switch (currentType
) {
834 defaultVoltageOut
= Voltage
.VOLTAGE_230
837 defaultVoltageOut
= Voltage
.VOLTAGE_400
840 logger
.error(`${logPrefix} ${errorMsg}`)
841 throw new BaseError(errorMsg
)
843 return defaultVoltageOut
846 export const getIdTagsFile
= (stationInfo
: ChargingStationInfo
): string | undefined => {
847 return stationInfo
.idTagsFile
!= null
848 ? join(dirname(fileURLToPath(import.meta
.url
)), 'assets', basename(stationInfo
.idTagsFile
))
852 export const waitChargingStationEvents
= async (
853 emitter
: EventEmitter
,
854 event
: ChargingStationWorkerMessageEvents
,
856 ): Promise
<number> => {
857 return await new Promise
<number>(resolve
=> {
859 if (eventsToWait
=== 0) {
863 emitter
.on(event
, () => {
865 if (events
=== eventsToWait
) {
872 const getConfiguredMaxNumberOfConnectors
= (stationTemplate
: ChargingStationTemplate
): number => {
873 let configuredMaxNumberOfConnectors
= 0
874 if (isNotEmptyArray
<number>(stationTemplate
.numberOfConnectors
)) {
875 const numberOfConnectors
= stationTemplate
.numberOfConnectors
876 configuredMaxNumberOfConnectors
=
877 numberOfConnectors
[Math.floor(secureRandom() * numberOfConnectors
.length
)]
878 } else if (typeof stationTemplate
.numberOfConnectors
=== 'number') {
879 configuredMaxNumberOfConnectors
= stationTemplate
.numberOfConnectors
880 } else if (stationTemplate
.Connectors
!= null && stationTemplate
.Evses
== null) {
881 configuredMaxNumberOfConnectors
=
882 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
883 stationTemplate
.Connectors
[0] != null
884 ? getMaxNumberOfConnectors(stationTemplate
.Connectors
) - 1
885 : getMaxNumberOfConnectors(stationTemplate
.Connectors
)
886 } else if (stationTemplate
.Evses
!= null && stationTemplate
.Connectors
== null) {
887 for (const evse
in stationTemplate
.Evses
) {
891 configuredMaxNumberOfConnectors
+= getMaxNumberOfConnectors(
892 stationTemplate
.Evses
[evse
].Connectors
896 return configuredMaxNumberOfConnectors
899 const checkConfiguredMaxConnectors
= (
900 configuredMaxConnectors
: number,
904 if (configuredMaxConnectors
<= 0) {
906 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors.toString()} connectors`
911 const checkTemplateMaxConnectors
= (
912 templateMaxConnectors
: number,
916 if (templateMaxConnectors
=== 0) {
918 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
920 } else if (templateMaxConnectors
< 0) {
922 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
927 const initializeConnectorStatus
= (connectorStatus
: ConnectorStatus
): void => {
928 connectorStatus
.availability
= AvailabilityType
.Operative
929 connectorStatus
.idTagLocalAuthorized
= false
930 connectorStatus
.idTagAuthorized
= false
931 connectorStatus
.transactionRemoteStarted
= false
932 connectorStatus
.transactionStarted
= false
933 connectorStatus
.energyActiveImportRegisterValue
= 0
934 connectorStatus
.transactionEnergyActiveImportRegisterValue
= 0
935 connectorStatus
.chargingProfiles
??= []
938 const warnDeprecatedTemplateKey
= (
939 template
: ChargingStationTemplate
,
942 templateFile
: string,
945 if (template
[key
as keyof ChargingStationTemplate
] != null) {
946 const logMsg
= `Deprecated template key '${key}' usage in file '${templateFile}'${
947 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}
` : ''
949 logger
.warn(`${logPrefix} ${logMsg}`)
950 console
.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
954 const convertDeprecatedTemplateKey
= (
955 template
: ChargingStationTemplate
,
956 deprecatedKey
: string,
959 if (template
[deprecatedKey
as keyof ChargingStationTemplate
] != null) {
961 ;(template
as unknown
as Record
<string, unknown
>)[key
] =
962 template
[deprecatedKey
as keyof ChargingStationTemplate
]
964 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
965 delete template
[deprecatedKey
as keyof ChargingStationTemplate
]
969 interface ChargingProfilesLimit
{
970 chargingProfile
: ChargingProfile
975 * Get the charging profiles limit for a connector
976 * Charging profiles shall already be sorted by priorities
977 * @param chargingStation -
978 * @param connectorId -
979 * @param chargingProfiles -
980 * @returns ChargingProfilesLimit
982 const getChargingProfilesLimit
= (
983 chargingStation
: ChargingStation
,
985 chargingProfiles
: ChargingProfile
[]
986 ): ChargingProfilesLimit
| undefined => {
987 const debugLogMsg
= `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profiles limit found: %j`
988 const currentDate
= new Date()
989 const connectorStatus
= chargingStation
.getConnectorStatus(connectorId
)
990 let previousActiveChargingProfile
: ChargingProfile
| undefined
991 for (const chargingProfile
of chargingProfiles
) {
992 const chargingSchedule
= chargingProfile
.chargingSchedule
993 if (chargingSchedule
.startSchedule
== null) {
995 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} has no startSchedule defined. Trying to set it to the connector current transaction start date`
997 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
998 chargingSchedule
.startSchedule
= connectorStatus
?.transactionStart
1000 if (!isDate(chargingSchedule
.startSchedule
)) {
1002 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
1004 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1005 chargingSchedule
.startSchedule
= convertToDate(chargingSchedule
.startSchedule
)!
1007 if (chargingSchedule
.duration
== null) {
1009 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} has no duration defined and will be set to the maximum time allowed`
1011 // OCPP specifies that if duration is not defined, it should be infinite
1012 chargingSchedule
.duration
= differenceInSeconds(maxTime
, chargingSchedule
.startSchedule
)
1015 !prepareChargingProfileKind(
1019 chargingStation
.logPrefix()
1024 if (!canProceedChargingProfile(chargingProfile
, currentDate
, chargingStation
.logPrefix())) {
1027 // Check if the charging profile is active
1029 isWithinInterval(currentDate
, {
1030 end
: addSeconds(chargingSchedule
.startSchedule
, chargingSchedule
.duration
),
1031 start
: chargingSchedule
.startSchedule
,
1034 if (isNotEmptyArray
<ChargingSchedulePeriod
>(chargingSchedule
.chargingSchedulePeriod
)) {
1035 const chargingSchedulePeriodCompareFn
= (
1036 a
: ChargingSchedulePeriod
,
1037 b
: ChargingSchedulePeriod
1038 ): number => a
.startPeriod
- b
.startPeriod
1040 !isArraySorted
<ChargingSchedulePeriod
>(
1041 chargingSchedule
.chargingSchedulePeriod
,
1042 chargingSchedulePeriodCompareFn
1046 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} schedule periods are not sorted by start period`
1048 chargingSchedule
.chargingSchedulePeriod
.sort(chargingSchedulePeriodCompareFn
)
1050 // Check if the first schedule period startPeriod property is equal to 0
1051 if (chargingSchedule
.chargingSchedulePeriod
[0].startPeriod
!== 0) {
1053 `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod.toString()} is not equal to 0`
1057 // Handle only one schedule period
1058 if (chargingSchedule
.chargingSchedulePeriod
.length
=== 1) {
1059 const chargingProfilesLimit
: ChargingProfilesLimit
= {
1061 limit
: chargingSchedule
.chargingSchedulePeriod
[0].limit
,
1063 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
1064 return chargingProfilesLimit
1066 let previousChargingSchedulePeriod
: ChargingSchedulePeriod
| undefined
1067 // Search for the right schedule period
1070 chargingSchedulePeriod
,
1071 ] of chargingSchedule
.chargingSchedulePeriod
.entries()) {
1072 // Find the right schedule period
1075 addSeconds(chargingSchedule
.startSchedule
, chargingSchedulePeriod
.startPeriod
),
1079 // Found the schedule period: previous is the correct one
1080 const chargingProfilesLimit
: ChargingProfilesLimit
= {
1081 chargingProfile
: previousActiveChargingProfile
?? chargingProfile
,
1082 limit
: previousChargingSchedulePeriod
?.limit
?? chargingSchedulePeriod
.limit
,
1084 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
1085 return chargingProfilesLimit
1087 // Handle the last schedule period within the charging profile duration
1089 index
=== chargingSchedule
.chargingSchedulePeriod
.length
- 1 ||
1090 (index
< chargingSchedule
.chargingSchedulePeriod
.length
- 1 &&
1091 differenceInSeconds(
1093 chargingSchedule
.startSchedule
,
1094 chargingSchedule
.chargingSchedulePeriod
[index
+ 1].startPeriod
1096 chargingSchedule
.startSchedule
1097 ) > chargingSchedule
.duration
)
1099 const chargingProfilesLimit
: ChargingProfilesLimit
= {
1101 limit
: chargingSchedulePeriod
.limit
,
1103 logger
.debug(debugLogMsg
, chargingProfilesLimit
)
1104 return chargingProfilesLimit
1106 // Keep a reference to previous charging schedule period
1107 previousChargingSchedulePeriod
= chargingSchedulePeriod
1110 // Keep a reference to previous active charging profile
1111 previousActiveChargingProfile
= chargingProfile
1116 export const prepareChargingProfileKind
= (
1117 connectorStatus
: ConnectorStatus
| undefined,
1118 chargingProfile
: ChargingProfile
,
1119 currentDate
: Date | number | string,
1122 switch (chargingProfile
.chargingProfileKind
) {
1123 case ChargingProfileKindType
.RECURRING
:
1124 if (!canProceedRecurringChargingProfile(chargingProfile
, logPrefix
)) {
1127 prepareRecurringChargingProfile(chargingProfile
, currentDate
, logPrefix
)
1129 case ChargingProfileKindType
.RELATIVE
:
1130 if (chargingProfile
.chargingSchedule
.startSchedule
!= null) {
1132 `${logPrefix} ${moduleName}.prepareChargingProfileKind: Relative charging profile id ${chargingProfile.chargingProfileId.toString()} has a startSchedule property defined. It will be ignored or used if the connector has a transaction started`
1134 delete chargingProfile
.chargingSchedule
.startSchedule
1136 if (connectorStatus
?.transactionStarted
=== true) {
1137 chargingProfile
.chargingSchedule
.startSchedule
= connectorStatus
.transactionStart
1139 // FIXME: handle relative charging profile duration
1145 export const canProceedChargingProfile
= (
1146 chargingProfile
: ChargingProfile
,
1147 currentDate
: Date | number | string,
1151 (isValidDate(chargingProfile
.validFrom
) && isBefore(currentDate
, chargingProfile
.validFrom
)) ||
1152 (isValidDate(chargingProfile
.validTo
) && isAfter(currentDate
, chargingProfile
.validTo
))
1155 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId.toString()} is not valid for the current date ${
1156 isDate(currentDate) ? currentDate.toISOString() : currentDate.toString()
1162 chargingProfile
.chargingSchedule
.startSchedule
== null ||
1163 chargingProfile
.chargingSchedule
.duration
== null
1166 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId.toString()} has no startSchedule or duration defined`
1170 if (!isValidDate(chargingProfile
.chargingSchedule
.startSchedule
)) {
1172 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId.toString()} has an invalid startSchedule date defined`
1176 if (!Number.isSafeInteger(chargingProfile
.chargingSchedule
.duration
)) {
1178 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId.toString()} has non integer duration defined`
1185 const canProceedRecurringChargingProfile
= (
1186 chargingProfile
: ChargingProfile
,
1190 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1191 chargingProfile
.recurrencyKind
== null
1194 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId.toString()} has no recurrencyKind defined`
1199 chargingProfile
.chargingProfileKind
=== ChargingProfileKindType
.RECURRING
&&
1200 chargingProfile
.chargingSchedule
.startSchedule
== null
1203 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId.toString()} has no startSchedule defined`
1211 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1212 * @param chargingProfile -
1213 * @param currentDate -
1214 * @param logPrefix -
1217 const prepareRecurringChargingProfile
= (
1218 chargingProfile
: ChargingProfile
,
1219 currentDate
: Date | number | string,
1222 const chargingSchedule
= chargingProfile
.chargingSchedule
1223 let recurringIntervalTranslated
= false
1224 let recurringInterval
: Interval
| undefined
1225 switch (chargingProfile
.recurrencyKind
) {
1226 case RecurrencyKindType
.DAILY
:
1227 recurringInterval
= {
1228 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1229 end
: addDays(chargingSchedule
.startSchedule
!, 1),
1230 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1231 start
: chargingSchedule
.startSchedule
!,
1233 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1235 !isWithinInterval(currentDate
, recurringInterval
) &&
1236 isBefore(recurringInterval
.end
, currentDate
)
1238 chargingSchedule
.startSchedule
= addDays(
1239 recurringInterval
.start
,
1240 differenceInDays(currentDate
, recurringInterval
.start
)
1242 recurringInterval
= {
1243 end
: addDays(chargingSchedule
.startSchedule
, 1),
1244 start
: chargingSchedule
.startSchedule
,
1246 recurringIntervalTranslated
= true
1249 case RecurrencyKindType
.WEEKLY
:
1250 recurringInterval
= {
1251 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1252 end
: addWeeks(chargingSchedule
.startSchedule
!, 1),
1253 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1254 start
: chargingSchedule
.startSchedule
!,
1256 checkRecurringChargingProfileDuration(chargingProfile
, recurringInterval
, logPrefix
)
1258 !isWithinInterval(currentDate
, recurringInterval
) &&
1259 isBefore(recurringInterval
.end
, currentDate
)
1261 chargingSchedule
.startSchedule
= addWeeks(
1262 recurringInterval
.start
,
1263 differenceInWeeks(currentDate
, recurringInterval
.start
)
1265 recurringInterval
= {
1266 end
: addWeeks(chargingSchedule
.startSchedule
, 1),
1267 start
: chargingSchedule
.startSchedule
,
1269 recurringIntervalTranslated
= true
1274 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1275 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId.toString()} is not supported`
1278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1279 if (recurringIntervalTranslated
&& !isWithinInterval(currentDate
, recurringInterval
!)) {
1281 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1282 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1283 chargingProfile.recurrencyKind
1284 } charging profile id ${chargingProfile.chargingProfileId.toString()} recurrency time interval [${toDate(
1285 recurringInterval?.start as Date
1286 ).toISOString()}, ${toDate(
1287 recurringInterval?.end as Date
1288 ).toISOString()}] has not been properly translated to current date ${
1289 isDate(currentDate) ? currentDate.toISOString() : currentDate.toString()
1293 return recurringIntervalTranslated
1296 const checkRecurringChargingProfileDuration
= (
1297 chargingProfile
: ChargingProfile
,
1301 if (chargingProfile
.chargingSchedule
.duration
== null) {
1303 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1304 chargingProfile.chargingProfileKind
1305 } charging profile id ${chargingProfile.chargingProfileId.toString()} duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1310 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1312 chargingProfile
.chargingSchedule
.duration
> differenceInSeconds(interval
.end
, interval
.start
)
1315 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1316 chargingProfile.chargingProfileKind
1317 } charging profile id ${chargingProfile.chargingProfileId.toString()} duration ${chargingProfile.chargingSchedule.duration.toString()} is greater than the recurrency time interval duration ${differenceInSeconds(
1322 chargingProfile
.chargingSchedule
.duration
= differenceInSeconds(interval
.end
, interval
.start
)
1326 const getRandomSerialNumberSuffix
= (params
?: {
1327 randomBytesLength
?: number
1330 const randomSerialNumberSuffix
= randomBytes(params
?.randomBytesLength
?? 16).toString('hex')
1331 if (params
?.upperCase
) {
1332 return randomSerialNumberSuffix
.toUpperCase()
1334 return randomSerialNumberSuffix