Merge branch 'main' into issue39-ocpp2
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
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'
6
7 import chalk from 'chalk'
8 import {
9 addDays,
10 addSeconds,
11 addWeeks,
12 differenceInDays,
13 differenceInSeconds,
14 differenceInWeeks,
15 type Interval,
16 isAfter,
17 isBefore,
18 isDate,
19 isPast,
20 isWithinInterval,
21 toDate
22 } from 'date-fns'
23 import { maxTime } from 'date-fns/constants'
24 import { isEmpty } from 'rambda'
25
26 import { BaseError } from '../exception/index.js'
27 import {
28 AmpereUnits,
29 AvailabilityType,
30 type BootNotificationRequest,
31 BootReasonEnumType,
32 type ChargingProfile,
33 ChargingProfileKindType,
34 ChargingProfilePurposeType,
35 ChargingRateUnitType,
36 type ChargingSchedulePeriod,
37 type ChargingStationConfiguration,
38 type ChargingStationInfo,
39 type ChargingStationOptions,
40 type ChargingStationTemplate,
41 type ChargingStationWorkerMessageEvents,
42 ConnectorPhaseRotation,
43 type ConnectorStatus,
44 ConnectorStatusEnum,
45 CurrentType,
46 type EvseTemplate,
47 type OCPP16BootNotificationRequest,
48 type OCPP20BootNotificationRequest,
49 OCPPVersion,
50 RecurrencyKindType,
51 type Reservation,
52 ReservationTerminationReason,
53 StandardParametersKey,
54 type SupportedFeatureProfiles,
55 Voltage
56 } from '../types/index.js'
57 import {
58 ACElectricUtils,
59 clone,
60 Constants,
61 convertToDate,
62 convertToInt,
63 DCElectricUtils,
64 isArraySorted,
65 isNotEmptyArray,
66 isNotEmptyString,
67 isValidDate,
68 logger,
69 secureRandom
70 } from '../utils/index.js'
71 import type { ChargingStation } from './ChargingStation.js'
72 import { getConfigurationKey } from './ConfigurationKeyUtils.js'
73
74 const moduleName = 'Helpers'
75
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')),
80 templateFile
81 )
82 }
83 const templateFileParsedPath = parse(templateFile)
84 return join(templateFileParsedPath.dir, templateFileParsedPath.name)
85 }
86
87 export const getChargingStationId = (
88 index: number,
89 stationTemplate: ChargingStationTemplate | undefined
90 ): string => {
91 if (stationTemplate == null) {
92 return "Unknown 'chargingStationId'"
93 }
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(
101 idStr.length - 4
102 )}${idSuffix}`
103 }
104
105 export const hasReservationExpired = (reservation: Reservation): boolean => {
106 return isPast(reservation.expiryDate)
107 }
108
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()) {
115 if (
116 connectorStatus.reservation != null &&
117 hasReservationExpired(connectorStatus.reservation)
118 ) {
119 await chargingStation.removeReservation(
120 connectorStatus.reservation,
121 ReservationTerminationReason.EXPIRED
122 )
123 }
124 }
125 }
126 } else {
127 for (const connectorStatus of chargingStation.connectors.values()) {
128 if (
129 connectorStatus.reservation != null &&
130 hasReservationExpired(connectorStatus.reservation)
131 ) {
132 await chargingStation.removeReservation(
133 connectorStatus.reservation,
134 ReservationTerminationReason.EXPIRED
135 )
136 }
137 }
138 }
139 }
140
141 export const getNumberOfReservableConnectors = (
142 connectors: Map<number, ConnectorStatus>
143 ): number => {
144 let numberOfReservableConnectors = 0
145 for (const [connectorId, connectorStatus] of connectors) {
146 if (connectorId === 0) {
147 continue
148 }
149 if (connectorStatus.status === ConnectorStatusEnum.Available) {
150 ++numberOfReservableConnectors
151 }
152 }
153 return numberOfReservableConnectors
154 }
155
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
162 }),
163 ...(stationTemplate.chargePointSerialNumberPrefix != null && {
164 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
165 }),
166 ...(stationTemplate.meterSerialNumberPrefix != null && {
167 meterSerialNumber: stationTemplate.meterSerialNumberPrefix
168 }),
169 ...(stationTemplate.meterType != null && {
170 meterType: stationTemplate.meterType
171 })
172 }
173 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
174 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
175 .digest('hex')
176 }
177
178 export const checkChargingStation = (
179 chargingStation: ChargingStation,
180 logPrefix: string
181 ): boolean => {
182 if (!chargingStation.started && !chargingStation.starting) {
183 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`)
184 return false
185 }
186 return true
187 }
188
189 export const getPhaseRotationValue = (
190 connectorId: number,
191 numberOfPhases: number
192 ): string | undefined => {
193 // AC/DC
194 if (connectorId === 0 && numberOfPhases === 0) {
195 return `${connectorId}.${ConnectorPhaseRotation.RST}`
196 } else if (connectorId > 0 && numberOfPhases === 0) {
197 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
198 // AC
199 } else if (connectorId >= 0 && numberOfPhases === 1) {
200 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
201 } else if (connectorId >= 0 && numberOfPhases === 3) {
202 return `${connectorId}.${ConnectorPhaseRotation.RST}`
203 }
204 }
205
206 export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate> | undefined): number => {
207 if (evses == null) {
208 return -1
209 }
210 return Object.keys(evses).length
211 }
212
213 const getMaxNumberOfConnectors = (
214 connectors: Record<string, ConnectorStatus> | undefined
215 ): number => {
216 if (connectors == null) {
217 return -1
218 }
219 return Object.keys(connectors).length
220 }
221
222 export const getBootConnectorStatus = (
223 chargingStation: ChargingStation,
224 connectorId: number,
225 connectorStatus: ConnectorStatus
226 ): ConnectorStatusEnum => {
227 let connectorBootStatus: ConnectorStatusEnum
228 if (
229 connectorStatus.status == null &&
230 (!chargingStation.isChargingStationAvailable() ||
231 !chargingStation.isConnectorAvailable(connectorId))
232 ) {
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
240 } else {
241 // Set default status
242 connectorBootStatus = ConnectorStatusEnum.Available
243 }
244 return connectorBootStatus
245 }
246
247 export const checkTemplate = (
248 stationTemplate: ChargingStationTemplate | undefined,
249 logPrefix: string,
250 templateFile: string
251 ): void => {
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)
256 }
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)
261 }
262 if (stationTemplate.idTagsFile == null || isEmpty(stationTemplate.idTagsFile)) {
263 logger.warn(
264 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
265 )
266 }
267 }
268
269 export const checkConfiguration = (
270 stationConfiguration: ChargingStationConfiguration | undefined,
271 logPrefix: string,
272 configurationFile: string
273 ): void => {
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)
278 }
279 if (isEmpty(stationConfiguration)) {
280 const errorMsg = `Empty charging station configuration from file ${configurationFile}`
281 logger.error(`${logPrefix} ${errorMsg}`)
282 throw new BaseError(errorMsg)
283 }
284 }
285
286 export const checkConnectorsConfiguration = (
287 stationTemplate: ChargingStationTemplate,
288 logPrefix: string,
289 templateFile: string
290 ): {
291 configuredMaxConnectors: number
292 templateMaxConnectors: number
293 templateMaxAvailableConnectors: number
294 } => {
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
301 if (
302 configuredMaxConnectors > templateMaxAvailableConnectors &&
303 stationTemplate.randomConnectors !== true
304 ) {
305 logger.warn(
306 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
307 )
308 stationTemplate.randomConnectors = true
309 }
310 return {
311 configuredMaxConnectors,
312 templateMaxConnectors,
313 templateMaxAvailableConnectors
314 }
315 }
316
317 export const checkStationInfoConnectorStatus = (
318 connectorId: number,
319 connectorStatus: ConnectorStatus,
320 logPrefix: string,
321 templateFile: string
322 ): void => {
323 if (connectorStatus.status != null) {
324 logger.warn(
325 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
326 )
327 delete connectorStatus.status
328 }
329 }
330
331 export const setChargingStationOptions = (
332 stationInfo: ChargingStationInfo,
333 options?: ChargingStationOptions
334 ): ChargingStationInfo => {
335 if (options?.supervisionUrls != null) {
336 stationInfo.supervisionUrls = options.supervisionUrls
337 }
338 if (options?.persistentConfiguration != null) {
339 stationInfo.stationInfoPersistentConfiguration = options.persistentConfiguration
340 stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
341 stationInfo.automaticTransactionGeneratorPersistentConfiguration =
342 options.persistentConfiguration
343 }
344 if (options?.autoStart != null) {
345 stationInfo.autoStart = options.autoStart
346 }
347 if (options?.autoRegister != null) {
348 stationInfo.autoRegister = options.autoRegister
349 }
350 if (options?.enableStatistics != null) {
351 stationInfo.enableStatistics = options.enableStatistics
352 }
353 if (options?.ocppStrictCompliance != null) {
354 stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
355 }
356 if (options?.stopTransactionsOnStopped != null) {
357 stationInfo.stopTransactionsOnStopped = options.stopTransactionsOnStopped
358 }
359 return stationInfo
360 }
361
362 export const buildConnectorsMap = (
363 connectors: Record<string, ConnectorStatus>,
364 logPrefix: string,
365 templateFile: string
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))
374 }
375 } else {
376 logger.warn(
377 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
378 )
379 }
380 return connectorsMap
381 }
382
383 export const initializeConnectorsMapStatus = (
384 connectors: Map<number, ConnectorStatus>,
385 logPrefix: string
386 ): void => {
387 for (const connectorId of connectors.keys()) {
388 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
389 logger.warn(
390 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
391 connectors.get(connectorId)?.transactionId
392 }`
393 )
394 }
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 = []
401 }
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)!)
405 }
406 }
407 }
408
409 export const resetAuthorizeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
410 connectorStatus.idTagLocalAuthorized = false
411 connectorStatus.idTagAuthorized = false
412 delete connectorStatus.localAuthorizeIdTag
413 delete connectorStatus.authorizeIdTag
414 }
415
416 export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefined): void => {
417 if (connectorStatus == null) {
418 return
419 }
420 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
421 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles.filter(
422 chargingProfile =>
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
428 )
429 }
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
438 }
439
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)!
444 }
445 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
446 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles
447 .filter(
448 chargingProfile =>
449 chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE
450 )
451 .map(chargingProfile => {
452 chargingProfile.chargingSchedule.startSchedule = convertToDate(
453 chargingProfile.chargingSchedule.startSchedule
454 )
455 chargingProfile.validFrom = convertToDate(chargingProfile.validFrom)
456 chargingProfile.validTo = convertToDate(chargingProfile.validTo)
457 return chargingProfile
458 })
459 }
460 return connectorStatus
461 }
462
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:
470 return {
471 chargePointModel: stationInfo.chargePointModel,
472 chargePointVendor: stationInfo.chargePointVendor,
473 ...(stationInfo.chargeBoxSerialNumber != null && {
474 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
475 }),
476 ...(stationInfo.chargePointSerialNumber != null && {
477 chargePointSerialNumber: stationInfo.chargePointSerialNumber
478 }),
479 ...(stationInfo.firmwareVersion != null && {
480 firmwareVersion: stationInfo.firmwareVersion
481 }),
482 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
483 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
484 ...(stationInfo.meterSerialNumber != null && {
485 meterSerialNumber: stationInfo.meterSerialNumber
486 }),
487 ...(stationInfo.meterType != null && {
488 meterType: stationInfo.meterType
489 })
490 } satisfies OCPP16BootNotificationRequest
491 case OCPPVersion.VERSION_20:
492 case OCPPVersion.VERSION_201:
493 return {
494 reason: bootReason,
495 chargingStation: {
496 model: stationInfo.chargePointModel,
497 vendorName: stationInfo.chargePointVendor,
498 ...(stationInfo.firmwareVersion != null && {
499 firmwareVersion: stationInfo.firmwareVersion
500 }),
501 ...(stationInfo.chargeBoxSerialNumber != null && {
502 serialNumber: stationInfo.chargeBoxSerialNumber
503 }),
504 ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
505 modem: {
506 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
507 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi })
508 }
509 })
510 }
511 } satisfies OCPP20BootNotificationRequest
512 }
513 }
514
515 export const warnTemplateKeysDeprecation = (
516 stationTemplate: ChargingStationTemplate,
517 logPrefix: string,
518 templateFile: string
519 ): void => {
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' }
525 ]
526 for (const templateKey of templateKeys) {
527 warnDeprecatedTemplateKey(
528 stationTemplate,
529 templateKey.deprecatedKey,
530 logPrefix,
531 templateFile,
532 templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
533 )
534 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
535 }
536 }
537
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
553 }
554
555 export const createSerialNumber = (
556 stationTemplate: ChargingStationTemplate,
557 stationInfo: ChargingStationInfo,
558 params?: {
559 randomSerialNumberUpperCase?: boolean
560 randomSerialNumber?: boolean
561 }
562 ): void => {
563 params = {
564 ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true },
565 ...params
566 }
567 const serialNumberSuffix =
568 params.randomSerialNumber === true
569 ? getRandomSerialNumberSuffix({
570 upperCase: params.randomSerialNumberUpperCase
571 })
572 : ''
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}`)
579 }
580
581 export const propagateSerialNumber = (
582 stationTemplate: ChargingStationTemplate | undefined,
583 stationInfoSrc: ChargingStationInfo | undefined,
584 stationInfoDst: ChargingStationInfo
585 ): void => {
586 if (stationInfoSrc == null || stationTemplate == null) {
587 throw new BaseError(
588 'Missing charging station template or existing configuration to propagate serial number'
589 )
590 }
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
603 }
604
605 export const hasFeatureProfile = (
606 chargingStation: ChargingStation,
607 featureProfile: SupportedFeatureProfiles
608 ): boolean | undefined => {
609 return getConfigurationKey(
610 chargingStation,
611 StandardParametersKey.SupportedFeatureProfiles
612 )?.value?.includes(featureProfile)
613 }
614
615 export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
616 let unitDivider = 1
617 switch (stationInfo.amperageLimitationUnit) {
618 case AmpereUnits.DECI_AMPERE:
619 unitDivider = 10
620 break
621 case AmpereUnits.CENTI_AMPERE:
622 unitDivider = 100
623 break
624 case AmpereUnits.MILLI_AMPERE:
625 unitDivider = 1000
626 break
627 }
628 return unitDivider
629 }
630
631 /**
632 * Gets the connector charging profiles relevant for power limitation shallow cloned and sorted by priorities
633 *
634 * @param chargingStation - Charging station
635 * @param connectorId - Connector id
636 * @returns connector charging profiles array
637 */
638 export const getConnectorChargingProfiles = (
639 chargingStation: ChargingStation,
640 connectorId: number
641 ): ChargingProfile[] => {
642 // FIXME: handle charging profile purpose CHARGE_POINT_MAX_PROFILE
643 return (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
644 .slice()
645 .sort((a, b) => {
646 if (
647 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE &&
648 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
649 ) {
650 return -1
651 } else if (
652 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE &&
653 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE
654 ) {
655 return 1
656 }
657 return b.stackLevel - a.stackLevel
658 })
659 .concat(
660 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [])
661 .filter(
662 chargingProfile =>
663 chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
664 )
665 .sort((a, b) => b.stackLevel - a.stackLevel)
666 )
667 }
668
669 export const getChargingStationConnectorChargingProfilesPowerLimit = (
670 chargingStation: ChargingStation,
671 connectorId: number
672 ): number | undefined => {
673 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
674 if (isNotEmptyArray(chargingProfiles)) {
675 const chargingProfilesLimit = getLimitFromChargingProfiles(
676 chargingStation,
677 connectorId,
678 chargingProfiles,
679 chargingStation.logPrefix()
680 )
681 if (chargingProfilesLimit != null) {
682 let { limit, chargingProfile } = chargingProfilesLimit
683 switch (chargingStation.stationInfo?.currentOutType) {
684 case CurrentType.AC:
685 limit =
686 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
687 ? limit
688 : ACElectricUtils.powerTotal(
689 chargingStation.getNumberOfPhases(),
690 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
691 chargingStation.stationInfo.voltageOut!,
692 limit
693 )
694 break
695 case CurrentType.DC:
696 limit =
697 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
698 ? limit
699 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
700 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
701 }
702 const connectorMaximumPower =
703 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
704 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
705 if (limit > connectorMaximumPower) {
706 logger.error(
707 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
708 chargingProfile.chargingProfileId
709 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
710 chargingProfilesLimit
711 )
712 limit = connectorMaximumPower
713 }
714 return limit
715 }
716 }
717 }
718
719 export const getDefaultVoltageOut = (
720 currentType: CurrentType,
721 logPrefix: string,
722 templateFile: string
723 ): Voltage => {
724 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
725 let defaultVoltageOut: number
726 switch (currentType) {
727 case CurrentType.AC:
728 defaultVoltageOut = Voltage.VOLTAGE_230
729 break
730 case CurrentType.DC:
731 defaultVoltageOut = Voltage.VOLTAGE_400
732 break
733 default:
734 logger.error(`${logPrefix} ${errorMsg}`)
735 throw new BaseError(errorMsg)
736 }
737 return defaultVoltageOut
738 }
739
740 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
741 return stationInfo.idTagsFile != null
742 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
743 : undefined
744 }
745
746 export const waitChargingStationEvents = async (
747 emitter: EventEmitter,
748 event: ChargingStationWorkerMessageEvents,
749 eventsToWait: number
750 ): Promise<number> => {
751 return await new Promise<number>(resolve => {
752 let events = 0
753 if (eventsToWait === 0) {
754 resolve(events)
755 return
756 }
757 emitter.on(event, () => {
758 ++events
759 if (events === eventsToWait) {
760 resolve(events)
761 }
762 })
763 })
764 }
765
766 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
767 let configuredMaxNumberOfConnectors = 0
768 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
769 const numberOfConnectors = stationTemplate.numberOfConnectors
770 configuredMaxNumberOfConnectors =
771 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
772 } else if (stationTemplate.numberOfConnectors != null) {
773 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
774 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
775 configuredMaxNumberOfConnectors =
776 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
777 stationTemplate.Connectors[0] != null
778 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
779 : getMaxNumberOfConnectors(stationTemplate.Connectors)
780 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
781 for (const evse in stationTemplate.Evses) {
782 if (evse === '0') {
783 continue
784 }
785 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
786 stationTemplate.Evses[evse].Connectors
787 )
788 }
789 }
790 return configuredMaxNumberOfConnectors
791 }
792
793 const checkConfiguredMaxConnectors = (
794 configuredMaxConnectors: number,
795 logPrefix: string,
796 templateFile: string
797 ): void => {
798 if (configuredMaxConnectors <= 0) {
799 logger.warn(
800 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
801 )
802 }
803 }
804
805 const checkTemplateMaxConnectors = (
806 templateMaxConnectors: number,
807 logPrefix: string,
808 templateFile: string
809 ): void => {
810 if (templateMaxConnectors === 0) {
811 logger.warn(
812 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
813 )
814 } else if (templateMaxConnectors < 0) {
815 logger.error(
816 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
817 )
818 }
819 }
820
821 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
822 connectorStatus.availability = AvailabilityType.Operative
823 connectorStatus.idTagLocalAuthorized = false
824 connectorStatus.idTagAuthorized = false
825 connectorStatus.transactionRemoteStarted = false
826 connectorStatus.transactionStarted = false
827 connectorStatus.energyActiveImportRegisterValue = 0
828 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
829 if (connectorStatus.chargingProfiles == null) {
830 connectorStatus.chargingProfiles = []
831 }
832 }
833
834 const warnDeprecatedTemplateKey = (
835 template: ChargingStationTemplate,
836 key: string,
837 logPrefix: string,
838 templateFile: string,
839 logMsgToAppend = ''
840 ): void => {
841 if (template[key as keyof ChargingStationTemplate] != null) {
842 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
843 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
844 }`
845 logger.warn(`${logPrefix} ${logMsg}`)
846 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
847 }
848 }
849
850 const convertDeprecatedTemplateKey = (
851 template: ChargingStationTemplate,
852 deprecatedKey: string,
853 key?: string
854 ): void => {
855 if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
856 if (key != null) {
857 (template as unknown as Record<string, unknown>)[key] =
858 template[deprecatedKey as keyof ChargingStationTemplate]
859 }
860 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
861 delete template[deprecatedKey as keyof ChargingStationTemplate]
862 }
863 }
864
865 interface ChargingProfilesLimit {
866 limit: number
867 chargingProfile: ChargingProfile
868 }
869
870 /**
871 * Charging profiles shall already be sorted by connector id descending then stack level descending
872 *
873 * @param chargingStation -
874 * @param connectorId -
875 * @param chargingProfiles -
876 * @param logPrefix -
877 * @returns ChargingProfilesLimit
878 */
879 const getLimitFromChargingProfiles = (
880 chargingStation: ChargingStation,
881 connectorId: number,
882 chargingProfiles: ChargingProfile[],
883 logPrefix: string
884 ): ChargingProfilesLimit | undefined => {
885 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profiles limit found: %j`
886 const currentDate = new Date()
887 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
888 let previousActiveChargingProfile: ChargingProfile | undefined
889 for (const chargingProfile of chargingProfiles) {
890 const chargingSchedule = chargingProfile.chargingSchedule
891 if (chargingSchedule.startSchedule == null) {
892 logger.debug(
893 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
894 )
895 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
896 chargingSchedule.startSchedule = connectorStatus?.transactionStart
897 }
898 if (!isDate(chargingSchedule.startSchedule)) {
899 logger.warn(
900 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
901 )
902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
903 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
904 }
905 if (chargingSchedule.duration == null) {
906 logger.debug(
907 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
908 )
909 // OCPP specifies that if duration is not defined, it should be infinite
910 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
911 }
912 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
913 continue
914 }
915 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
916 continue
917 }
918 // Check if the charging profile is active
919 if (
920 isWithinInterval(currentDate, {
921 start: chargingSchedule.startSchedule,
922 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
923 })
924 ) {
925 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
926 const chargingSchedulePeriodCompareFn = (
927 a: ChargingSchedulePeriod,
928 b: ChargingSchedulePeriod
929 ): number => a.startPeriod - b.startPeriod
930 if (
931 !isArraySorted<ChargingSchedulePeriod>(
932 chargingSchedule.chargingSchedulePeriod,
933 chargingSchedulePeriodCompareFn
934 )
935 ) {
936 logger.warn(
937 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
938 )
939 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
940 }
941 // Check if the first schedule period startPeriod property is equal to 0
942 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
943 logger.error(
944 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
945 )
946 continue
947 }
948 // Handle only one schedule period
949 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
950 const chargingProfilesLimit: ChargingProfilesLimit = {
951 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
952 chargingProfile
953 }
954 logger.debug(debugLogMsg, chargingProfilesLimit)
955 return chargingProfilesLimit
956 }
957 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
958 // Search for the right schedule period
959 for (const [
960 index,
961 chargingSchedulePeriod
962 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
963 // Find the right schedule period
964 if (
965 isAfter(
966 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
967 currentDate
968 )
969 ) {
970 // Found the schedule period: previous is the correct one
971 const chargingProfilesLimit: ChargingProfilesLimit = {
972 limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
973 chargingProfile: previousActiveChargingProfile ?? chargingProfile
974 }
975 logger.debug(debugLogMsg, chargingProfilesLimit)
976 return chargingProfilesLimit
977 }
978 // Handle the last schedule period within the charging profile duration
979 if (
980 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
981 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
982 differenceInSeconds(
983 addSeconds(
984 chargingSchedule.startSchedule,
985 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
986 ),
987 chargingSchedule.startSchedule
988 ) > chargingSchedule.duration)
989 ) {
990 const chargingProfilesLimit: ChargingProfilesLimit = {
991 limit: chargingSchedulePeriod.limit,
992 chargingProfile
993 }
994 logger.debug(debugLogMsg, chargingProfilesLimit)
995 return chargingProfilesLimit
996 }
997 // Keep a reference to previous charging schedule period
998 previousChargingSchedulePeriod = chargingSchedulePeriod
999 }
1000 }
1001 // Keep a reference to previous active charging profile
1002 previousActiveChargingProfile = chargingProfile
1003 }
1004 }
1005 }
1006
1007 export const prepareChargingProfileKind = (
1008 connectorStatus: ConnectorStatus | undefined,
1009 chargingProfile: ChargingProfile,
1010 currentDate: string | number | Date,
1011 logPrefix: string
1012 ): boolean => {
1013 switch (chargingProfile.chargingProfileKind) {
1014 case ChargingProfileKindType.RECURRING:
1015 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
1016 return false
1017 }
1018 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
1019 break
1020 case ChargingProfileKindType.RELATIVE:
1021 if (chargingProfile.chargingSchedule.startSchedule != null) {
1022 logger.warn(
1023 `${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`
1024 )
1025 delete chargingProfile.chargingSchedule.startSchedule
1026 }
1027 if (connectorStatus?.transactionStarted === true) {
1028 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
1029 }
1030 // FIXME: handle relative charging profile duration
1031 break
1032 }
1033 return true
1034 }
1035
1036 export const canProceedChargingProfile = (
1037 chargingProfile: ChargingProfile,
1038 currentDate: string | number | Date,
1039 logPrefix: string
1040 ): boolean => {
1041 if (
1042 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
1043 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
1044 ) {
1045 logger.debug(
1046 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1047 chargingProfile.chargingProfileId
1048 } is not valid for the current date ${
1049 isDate(currentDate) ? currentDate.toISOString() : currentDate
1050 }`
1051 )
1052 return false
1053 }
1054 if (
1055 chargingProfile.chargingSchedule.startSchedule == null ||
1056 chargingProfile.chargingSchedule.duration == null
1057 ) {
1058 logger.error(
1059 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1060 )
1061 return false
1062 }
1063 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
1064 logger.error(
1065 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1066 )
1067 return false
1068 }
1069 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
1070 logger.error(
1071 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1072 )
1073 return false
1074 }
1075 return true
1076 }
1077
1078 const canProceedRecurringChargingProfile = (
1079 chargingProfile: ChargingProfile,
1080 logPrefix: string
1081 ): boolean => {
1082 if (
1083 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1084 chargingProfile.recurrencyKind == null
1085 ) {
1086 logger.error(
1087 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1088 )
1089 return false
1090 }
1091 if (
1092 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1093 chargingProfile.chargingSchedule.startSchedule == null
1094 ) {
1095 logger.error(
1096 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1097 )
1098 return false
1099 }
1100 return true
1101 }
1102
1103 /**
1104 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1105 *
1106 * @param chargingProfile -
1107 * @param currentDate -
1108 * @param logPrefix -
1109 */
1110 const prepareRecurringChargingProfile = (
1111 chargingProfile: ChargingProfile,
1112 currentDate: string | number | Date,
1113 logPrefix: string
1114 ): boolean => {
1115 const chargingSchedule = chargingProfile.chargingSchedule
1116 let recurringIntervalTranslated = false
1117 let recurringInterval: Interval | undefined
1118 switch (chargingProfile.recurrencyKind) {
1119 case RecurrencyKindType.DAILY:
1120 recurringInterval = {
1121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1122 start: chargingSchedule.startSchedule!,
1123 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1124 end: addDays(chargingSchedule.startSchedule!, 1)
1125 }
1126 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1127 if (
1128 !isWithinInterval(currentDate, recurringInterval) &&
1129 isBefore(recurringInterval.end, currentDate)
1130 ) {
1131 chargingSchedule.startSchedule = addDays(
1132 recurringInterval.start,
1133 differenceInDays(currentDate, recurringInterval.start)
1134 )
1135 recurringInterval = {
1136 start: chargingSchedule.startSchedule,
1137 end: addDays(chargingSchedule.startSchedule, 1)
1138 }
1139 recurringIntervalTranslated = true
1140 }
1141 break
1142 case RecurrencyKindType.WEEKLY:
1143 recurringInterval = {
1144 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1145 start: chargingSchedule.startSchedule!,
1146 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1147 end: addWeeks(chargingSchedule.startSchedule!, 1)
1148 }
1149 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1150 if (
1151 !isWithinInterval(currentDate, recurringInterval) &&
1152 isBefore(recurringInterval.end, currentDate)
1153 ) {
1154 chargingSchedule.startSchedule = addWeeks(
1155 recurringInterval.start,
1156 differenceInWeeks(currentDate, recurringInterval.start)
1157 )
1158 recurringInterval = {
1159 start: chargingSchedule.startSchedule,
1160 end: addWeeks(chargingSchedule.startSchedule, 1)
1161 }
1162 recurringIntervalTranslated = true
1163 }
1164 break
1165 default:
1166 logger.error(
1167 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1168 )
1169 }
1170 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1171 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
1172 logger.error(
1173 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1174 chargingProfile.recurrencyKind
1175 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1176 recurringInterval?.start as Date
1177 ).toISOString()}, ${toDate(
1178 recurringInterval?.end as Date
1179 ).toISOString()}] has not been properly translated to current date ${
1180 isDate(currentDate) ? currentDate.toISOString() : currentDate
1181 } `
1182 )
1183 }
1184 return recurringIntervalTranslated
1185 }
1186
1187 const checkRecurringChargingProfileDuration = (
1188 chargingProfile: ChargingProfile,
1189 interval: Interval,
1190 logPrefix: string
1191 ): void => {
1192 if (chargingProfile.chargingSchedule.duration == null) {
1193 logger.warn(
1194 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1195 chargingProfile.chargingProfileKind
1196 } charging profile id ${
1197 chargingProfile.chargingProfileId
1198 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1199 interval.end,
1200 interval.start
1201 )}`
1202 )
1203 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1204 } else if (
1205 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
1206 ) {
1207 logger.warn(
1208 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1209 chargingProfile.chargingProfileKind
1210 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1211 chargingProfile.chargingSchedule.duration
1212 } is greater than the recurrency time interval duration ${differenceInSeconds(
1213 interval.end,
1214 interval.start
1215 )}`
1216 )
1217 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1218 }
1219 }
1220
1221 const getRandomSerialNumberSuffix = (params?: {
1222 randomBytesLength?: number
1223 upperCase?: boolean
1224 }): string => {
1225 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1226 if (params?.upperCase === true) {
1227 return randomSerialNumberSuffix.toUpperCase()
1228 }
1229 return randomSerialNumberSuffix
1230 }