feat: handle charging profile purpose TxProfile
[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 )
428 }
429 resetAuthorizeConnectorStatus(connectorStatus)
430 connectorStatus.transactionRemoteStarted = false
431 connectorStatus.transactionStarted = false
432 delete connectorStatus.transactionStart
433 delete connectorStatus.transactionId
434 delete connectorStatus.transactionIdTag
435 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
436 delete connectorStatus.transactionBeginMeterValue
437 }
438
439 export const prepareDatesInConnectorStatus = (
440 connectorStatus: ConnectorStatus
441 ): ConnectorStatus => {
442 if (connectorStatus.reservation != null) {
443 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
444 connectorStatus.reservation.expiryDate = convertToDate(connectorStatus.reservation.expiryDate)!
445 }
446 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
447 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles.map(chargingProfile => {
448 chargingProfile.chargingSchedule.startSchedule = convertToDate(
449 chargingProfile.chargingSchedule.startSchedule
450 )
451 chargingProfile.validFrom = convertToDate(chargingProfile.validFrom)
452 chargingProfile.validTo = convertToDate(chargingProfile.validTo)
453 return chargingProfile
454 })
455 }
456 return connectorStatus
457 }
458
459 export const createBootNotificationRequest = (
460 stationInfo: ChargingStationInfo,
461 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
462 ): BootNotificationRequest | undefined => {
463 const ocppVersion = stationInfo.ocppVersion
464 switch (ocppVersion) {
465 case OCPPVersion.VERSION_16:
466 return {
467 chargePointModel: stationInfo.chargePointModel,
468 chargePointVendor: stationInfo.chargePointVendor,
469 ...(stationInfo.chargeBoxSerialNumber != null && {
470 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
471 }),
472 ...(stationInfo.chargePointSerialNumber != null && {
473 chargePointSerialNumber: stationInfo.chargePointSerialNumber
474 }),
475 ...(stationInfo.firmwareVersion != null && {
476 firmwareVersion: stationInfo.firmwareVersion
477 }),
478 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
479 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
480 ...(stationInfo.meterSerialNumber != null && {
481 meterSerialNumber: stationInfo.meterSerialNumber
482 }),
483 ...(stationInfo.meterType != null && {
484 meterType: stationInfo.meterType
485 })
486 } satisfies OCPP16BootNotificationRequest
487 case OCPPVersion.VERSION_20:
488 case OCPPVersion.VERSION_201:
489 return {
490 reason: bootReason,
491 chargingStation: {
492 model: stationInfo.chargePointModel,
493 vendorName: stationInfo.chargePointVendor,
494 ...(stationInfo.firmwareVersion != null && {
495 firmwareVersion: stationInfo.firmwareVersion
496 }),
497 ...(stationInfo.chargeBoxSerialNumber != null && {
498 serialNumber: stationInfo.chargeBoxSerialNumber
499 }),
500 ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
501 modem: {
502 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
503 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi })
504 }
505 })
506 }
507 } satisfies OCPP20BootNotificationRequest
508 }
509 }
510
511 export const warnTemplateKeysDeprecation = (
512 stationTemplate: ChargingStationTemplate,
513 logPrefix: string,
514 templateFile: string
515 ): void => {
516 const templateKeys: Array<{ deprecatedKey: string, key?: string }> = [
517 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
518 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
519 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
520 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' }
521 ]
522 for (const templateKey of templateKeys) {
523 warnDeprecatedTemplateKey(
524 stationTemplate,
525 templateKey.deprecatedKey,
526 logPrefix,
527 templateFile,
528 templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
529 )
530 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
531 }
532 }
533
534 export const stationTemplateToStationInfo = (
535 stationTemplate: ChargingStationTemplate
536 ): ChargingStationInfo => {
537 stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
538 delete stationTemplate.power
539 delete stationTemplate.powerUnit
540 delete stationTemplate.Connectors
541 delete stationTemplate.Evses
542 delete stationTemplate.Configuration
543 delete stationTemplate.AutomaticTransactionGenerator
544 delete stationTemplate.numberOfConnectors
545 delete stationTemplate.chargeBoxSerialNumberPrefix
546 delete stationTemplate.chargePointSerialNumberPrefix
547 delete stationTemplate.meterSerialNumberPrefix
548 return stationTemplate as ChargingStationInfo
549 }
550
551 export const createSerialNumber = (
552 stationTemplate: ChargingStationTemplate,
553 stationInfo: ChargingStationInfo,
554 params?: {
555 randomSerialNumberUpperCase?: boolean
556 randomSerialNumber?: boolean
557 }
558 ): void => {
559 params = {
560 ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true },
561 ...params
562 }
563 const serialNumberSuffix =
564 params.randomSerialNumber === true
565 ? getRandomSerialNumberSuffix({
566 upperCase: params.randomSerialNumberUpperCase
567 })
568 : ''
569 isNotEmptyString(stationTemplate.chargePointSerialNumberPrefix) &&
570 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
571 isNotEmptyString(stationTemplate.chargeBoxSerialNumberPrefix) &&
572 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
573 isNotEmptyString(stationTemplate.meterSerialNumberPrefix) &&
574 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
575 }
576
577 export const propagateSerialNumber = (
578 stationTemplate: ChargingStationTemplate | undefined,
579 stationInfoSrc: ChargingStationInfo | undefined,
580 stationInfoDst: ChargingStationInfo
581 ): void => {
582 if (stationInfoSrc == null || stationTemplate == null) {
583 throw new BaseError(
584 'Missing charging station template or existing configuration to propagate serial number'
585 )
586 }
587 stationTemplate.chargePointSerialNumberPrefix != null &&
588 stationInfoSrc.chargePointSerialNumber != null
589 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
590 : stationInfoDst.chargePointSerialNumber != null &&
591 delete stationInfoDst.chargePointSerialNumber
592 stationTemplate.chargeBoxSerialNumberPrefix != null &&
593 stationInfoSrc.chargeBoxSerialNumber != null
594 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
595 : stationInfoDst.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
596 stationTemplate.meterSerialNumberPrefix != null && stationInfoSrc.meterSerialNumber != null
597 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
598 : stationInfoDst.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
599 }
600
601 export const hasFeatureProfile = (
602 chargingStation: ChargingStation,
603 featureProfile: SupportedFeatureProfiles
604 ): boolean | undefined => {
605 return getConfigurationKey(
606 chargingStation,
607 StandardParametersKey.SupportedFeatureProfiles
608 )?.value?.includes(featureProfile)
609 }
610
611 export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
612 let unitDivider = 1
613 switch (stationInfo.amperageLimitationUnit) {
614 case AmpereUnits.DECI_AMPERE:
615 unitDivider = 10
616 break
617 case AmpereUnits.CENTI_AMPERE:
618 unitDivider = 100
619 break
620 case AmpereUnits.MILLI_AMPERE:
621 unitDivider = 1000
622 break
623 }
624 return unitDivider
625 }
626
627 /**
628 * Gets the connector charging profiles relevant for power limitation shallow cloned and sorted by priorities
629 *
630 * @param chargingStation - Charging station
631 * @param connectorId - Connector id
632 * @returns connector charging profiles array
633 */
634 export const getConnectorChargingProfiles = (
635 chargingStation: ChargingStation,
636 connectorId: number
637 ): ChargingProfile[] => {
638 // FIXME: handle charging profile purpose CHARGE_POINT_MAX_PROFILE
639 return (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
640 .slice()
641 .sort((a, b) => {
642 if (
643 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE &&
644 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
645 ) {
646 return -1
647 } else if (
648 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE &&
649 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE
650 ) {
651 return 1
652 }
653 return b.stackLevel - a.stackLevel
654 })
655 .concat(
656 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [])
657 .filter(
658 chargingProfile =>
659 chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
660 )
661 .sort((a, b) => b.stackLevel - a.stackLevel)
662 )
663 }
664
665 export const getChargingStationConnectorChargingProfilesPowerLimit = (
666 chargingStation: ChargingStation,
667 connectorId: number
668 ): number | undefined => {
669 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
670 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
671 if (isNotEmptyArray(chargingProfiles)) {
672 const result = getLimitFromChargingProfiles(
673 chargingStation,
674 connectorId,
675 chargingProfiles,
676 chargingStation.logPrefix()
677 )
678 if (result != null) {
679 limit = result.limit
680 chargingProfile = result.chargingProfile
681 switch (chargingStation.stationInfo?.currentOutType) {
682 case CurrentType.AC:
683 limit =
684 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
685 ? limit
686 : ACElectricUtils.powerTotal(
687 chargingStation.getNumberOfPhases(),
688 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
689 chargingStation.stationInfo.voltageOut!,
690 limit
691 )
692 break
693 case CurrentType.DC:
694 limit =
695 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
696 ? limit
697 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
698 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
699 }
700 const connectorMaximumPower =
701 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
702 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
703 if (limit > connectorMaximumPower) {
704 logger.error(
705 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
706 chargingProfile.chargingProfileId
707 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
708 result
709 )
710 limit = connectorMaximumPower
711 }
712 }
713 }
714 return limit
715 }
716
717 export const getDefaultVoltageOut = (
718 currentType: CurrentType,
719 logPrefix: string,
720 templateFile: string
721 ): Voltage => {
722 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
723 let defaultVoltageOut: number
724 switch (currentType) {
725 case CurrentType.AC:
726 defaultVoltageOut = Voltage.VOLTAGE_230
727 break
728 case CurrentType.DC:
729 defaultVoltageOut = Voltage.VOLTAGE_400
730 break
731 default:
732 logger.error(`${logPrefix} ${errorMsg}`)
733 throw new BaseError(errorMsg)
734 }
735 return defaultVoltageOut
736 }
737
738 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
739 return stationInfo.idTagsFile != null
740 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
741 : undefined
742 }
743
744 export const waitChargingStationEvents = async (
745 emitter: EventEmitter,
746 event: ChargingStationWorkerMessageEvents,
747 eventsToWait: number
748 ): Promise<number> => {
749 return await new Promise<number>(resolve => {
750 let events = 0
751 if (eventsToWait === 0) {
752 resolve(events)
753 return
754 }
755 emitter.on(event, () => {
756 ++events
757 if (events === eventsToWait) {
758 resolve(events)
759 }
760 })
761 })
762 }
763
764 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
765 let configuredMaxNumberOfConnectors = 0
766 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
767 const numberOfConnectors = stationTemplate.numberOfConnectors
768 configuredMaxNumberOfConnectors =
769 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
770 } else if (stationTemplate.numberOfConnectors != null) {
771 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
772 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
773 configuredMaxNumberOfConnectors =
774 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
775 stationTemplate.Connectors[0] != null
776 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
777 : getMaxNumberOfConnectors(stationTemplate.Connectors)
778 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
779 for (const evse in stationTemplate.Evses) {
780 if (evse === '0') {
781 continue
782 }
783 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
784 stationTemplate.Evses[evse].Connectors
785 )
786 }
787 }
788 return configuredMaxNumberOfConnectors
789 }
790
791 const checkConfiguredMaxConnectors = (
792 configuredMaxConnectors: number,
793 logPrefix: string,
794 templateFile: string
795 ): void => {
796 if (configuredMaxConnectors <= 0) {
797 logger.warn(
798 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
799 )
800 }
801 }
802
803 const checkTemplateMaxConnectors = (
804 templateMaxConnectors: number,
805 logPrefix: string,
806 templateFile: string
807 ): void => {
808 if (templateMaxConnectors === 0) {
809 logger.warn(
810 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
811 )
812 } else if (templateMaxConnectors < 0) {
813 logger.error(
814 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
815 )
816 }
817 }
818
819 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
820 connectorStatus.availability = AvailabilityType.Operative
821 connectorStatus.idTagLocalAuthorized = false
822 connectorStatus.idTagAuthorized = false
823 connectorStatus.transactionRemoteStarted = false
824 connectorStatus.transactionStarted = false
825 connectorStatus.energyActiveImportRegisterValue = 0
826 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
827 if (connectorStatus.chargingProfiles == null) {
828 connectorStatus.chargingProfiles = []
829 }
830 }
831
832 const warnDeprecatedTemplateKey = (
833 template: ChargingStationTemplate,
834 key: string,
835 logPrefix: string,
836 templateFile: string,
837 logMsgToAppend = ''
838 ): void => {
839 if (template[key as keyof ChargingStationTemplate] != null) {
840 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
841 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
842 }`
843 logger.warn(`${logPrefix} ${logMsg}`)
844 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
845 }
846 }
847
848 const convertDeprecatedTemplateKey = (
849 template: ChargingStationTemplate,
850 deprecatedKey: string,
851 key?: string
852 ): void => {
853 if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
854 if (key != null) {
855 (template as unknown as Record<string, unknown>)[key] =
856 template[deprecatedKey as keyof ChargingStationTemplate]
857 }
858 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
859 delete template[deprecatedKey as keyof ChargingStationTemplate]
860 }
861 }
862
863 interface ChargingProfilesLimit {
864 limit: number
865 chargingProfile: ChargingProfile
866 }
867
868 /**
869 * Charging profiles shall already be sorted by connector id descending then stack level descending
870 *
871 * @param chargingStation -
872 * @param connectorId -
873 * @param chargingProfiles -
874 * @param logPrefix -
875 * @returns ChargingProfilesLimit
876 */
877 const getLimitFromChargingProfiles = (
878 chargingStation: ChargingStation,
879 connectorId: number,
880 chargingProfiles: ChargingProfile[],
881 logPrefix: string
882 ): ChargingProfilesLimit | undefined => {
883 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
884 const currentDate = new Date()
885 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
886 let previousActiveChargingProfile: ChargingProfile | undefined
887 for (const chargingProfile of chargingProfiles) {
888 const chargingSchedule = chargingProfile.chargingSchedule
889 if (chargingSchedule.startSchedule == null) {
890 logger.debug(
891 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
892 )
893 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
894 chargingSchedule.startSchedule = connectorStatus?.transactionStart
895 }
896 if (!isDate(chargingSchedule.startSchedule)) {
897 logger.warn(
898 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
899 )
900 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
901 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
902 }
903 if (chargingSchedule.duration == null) {
904 logger.debug(
905 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
906 )
907 // OCPP specifies that if duration is not defined, it should be infinite
908 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
909 }
910 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
911 continue
912 }
913 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
914 continue
915 }
916 // Check if the charging profile is active
917 if (
918 isWithinInterval(currentDate, {
919 start: chargingSchedule.startSchedule,
920 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
921 })
922 ) {
923 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
924 const chargingSchedulePeriodCompareFn = (
925 a: ChargingSchedulePeriod,
926 b: ChargingSchedulePeriod
927 ): number => a.startPeriod - b.startPeriod
928 if (
929 !isArraySorted<ChargingSchedulePeriod>(
930 chargingSchedule.chargingSchedulePeriod,
931 chargingSchedulePeriodCompareFn
932 )
933 ) {
934 logger.warn(
935 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
936 )
937 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
938 }
939 // Check if the first schedule period startPeriod property is equal to 0
940 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
941 logger.error(
942 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
943 )
944 continue
945 }
946 // Handle only one schedule period
947 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
948 const result: ChargingProfilesLimit = {
949 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
950 chargingProfile
951 }
952 logger.debug(debugLogMsg, result)
953 return result
954 }
955 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
956 // Search for the right schedule period
957 for (const [
958 index,
959 chargingSchedulePeriod
960 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
961 // Find the right schedule period
962 if (
963 isAfter(
964 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
965 currentDate
966 )
967 ) {
968 // Found the schedule period: previous is the correct one
969 const result: ChargingProfilesLimit = {
970 limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
971 chargingProfile: previousActiveChargingProfile ?? chargingProfile
972 }
973 logger.debug(debugLogMsg, result)
974 return result
975 }
976 // Handle the last schedule period within the charging profile duration
977 if (
978 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
979 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
980 differenceInSeconds(
981 addSeconds(
982 chargingSchedule.startSchedule,
983 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
984 ),
985 chargingSchedule.startSchedule
986 ) > chargingSchedule.duration)
987 ) {
988 const result: ChargingProfilesLimit = {
989 limit: chargingSchedulePeriod.limit,
990 chargingProfile
991 }
992 logger.debug(debugLogMsg, result)
993 return result
994 }
995 // Keep a reference to previous charging schedule period
996 previousChargingSchedulePeriod = chargingSchedulePeriod
997 }
998 }
999 // Keep a reference to previous active charging profile
1000 previousActiveChargingProfile = chargingProfile
1001 }
1002 }
1003 }
1004
1005 export const prepareChargingProfileKind = (
1006 connectorStatus: ConnectorStatus | undefined,
1007 chargingProfile: ChargingProfile,
1008 currentDate: string | number | Date,
1009 logPrefix: string
1010 ): boolean => {
1011 switch (chargingProfile.chargingProfileKind) {
1012 case ChargingProfileKindType.RECURRING:
1013 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
1014 return false
1015 }
1016 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
1017 break
1018 case ChargingProfileKindType.RELATIVE:
1019 if (chargingProfile.chargingSchedule.startSchedule != null) {
1020 logger.warn(
1021 `${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`
1022 )
1023 delete chargingProfile.chargingSchedule.startSchedule
1024 }
1025 if (connectorStatus?.transactionStarted === true) {
1026 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
1027 }
1028 // FIXME: handle relative charging profile duration
1029 break
1030 }
1031 return true
1032 }
1033
1034 export const canProceedChargingProfile = (
1035 chargingProfile: ChargingProfile,
1036 currentDate: string | number | Date,
1037 logPrefix: string
1038 ): boolean => {
1039 if (
1040 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
1041 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
1042 ) {
1043 logger.debug(
1044 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1045 chargingProfile.chargingProfileId
1046 } is not valid for the current date ${
1047 isDate(currentDate) ? currentDate.toISOString() : currentDate
1048 }`
1049 )
1050 return false
1051 }
1052 if (
1053 chargingProfile.chargingSchedule.startSchedule == null ||
1054 chargingProfile.chargingSchedule.duration == null
1055 ) {
1056 logger.error(
1057 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1058 )
1059 return false
1060 }
1061 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
1062 logger.error(
1063 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1064 )
1065 return false
1066 }
1067 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
1068 logger.error(
1069 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1070 )
1071 return false
1072 }
1073 return true
1074 }
1075
1076 const canProceedRecurringChargingProfile = (
1077 chargingProfile: ChargingProfile,
1078 logPrefix: string
1079 ): boolean => {
1080 if (
1081 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1082 chargingProfile.recurrencyKind == null
1083 ) {
1084 logger.error(
1085 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1086 )
1087 return false
1088 }
1089 if (
1090 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1091 chargingProfile.chargingSchedule.startSchedule == null
1092 ) {
1093 logger.error(
1094 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1095 )
1096 return false
1097 }
1098 return true
1099 }
1100
1101 /**
1102 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1103 *
1104 * @param chargingProfile -
1105 * @param currentDate -
1106 * @param logPrefix -
1107 */
1108 const prepareRecurringChargingProfile = (
1109 chargingProfile: ChargingProfile,
1110 currentDate: string | number | Date,
1111 logPrefix: string
1112 ): boolean => {
1113 const chargingSchedule = chargingProfile.chargingSchedule
1114 let recurringIntervalTranslated = false
1115 let recurringInterval: Interval | undefined
1116 switch (chargingProfile.recurrencyKind) {
1117 case RecurrencyKindType.DAILY:
1118 recurringInterval = {
1119 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1120 start: chargingSchedule.startSchedule!,
1121 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1122 end: addDays(chargingSchedule.startSchedule!, 1)
1123 }
1124 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1125 if (
1126 !isWithinInterval(currentDate, recurringInterval) &&
1127 isBefore(recurringInterval.end, currentDate)
1128 ) {
1129 chargingSchedule.startSchedule = addDays(
1130 recurringInterval.start,
1131 differenceInDays(currentDate, recurringInterval.start)
1132 )
1133 recurringInterval = {
1134 start: chargingSchedule.startSchedule,
1135 end: addDays(chargingSchedule.startSchedule, 1)
1136 }
1137 recurringIntervalTranslated = true
1138 }
1139 break
1140 case RecurrencyKindType.WEEKLY:
1141 recurringInterval = {
1142 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1143 start: chargingSchedule.startSchedule!,
1144 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1145 end: addWeeks(chargingSchedule.startSchedule!, 1)
1146 }
1147 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1148 if (
1149 !isWithinInterval(currentDate, recurringInterval) &&
1150 isBefore(recurringInterval.end, currentDate)
1151 ) {
1152 chargingSchedule.startSchedule = addWeeks(
1153 recurringInterval.start,
1154 differenceInWeeks(currentDate, recurringInterval.start)
1155 )
1156 recurringInterval = {
1157 start: chargingSchedule.startSchedule,
1158 end: addWeeks(chargingSchedule.startSchedule, 1)
1159 }
1160 recurringIntervalTranslated = true
1161 }
1162 break
1163 default:
1164 logger.error(
1165 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1166 )
1167 }
1168 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1169 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
1170 logger.error(
1171 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1172 chargingProfile.recurrencyKind
1173 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1174 recurringInterval?.start as Date
1175 ).toISOString()}, ${toDate(
1176 recurringInterval?.end as Date
1177 ).toISOString()}] has not been properly translated to current date ${
1178 isDate(currentDate) ? currentDate.toISOString() : currentDate
1179 } `
1180 )
1181 }
1182 return recurringIntervalTranslated
1183 }
1184
1185 const checkRecurringChargingProfileDuration = (
1186 chargingProfile: ChargingProfile,
1187 interval: Interval,
1188 logPrefix: string
1189 ): void => {
1190 if (chargingProfile.chargingSchedule.duration == null) {
1191 logger.warn(
1192 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1193 chargingProfile.chargingProfileKind
1194 } charging profile id ${
1195 chargingProfile.chargingProfileId
1196 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1197 interval.end,
1198 interval.start
1199 )}`
1200 )
1201 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1202 } else if (
1203 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
1204 ) {
1205 logger.warn(
1206 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1207 chargingProfile.chargingProfileKind
1208 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1209 chargingProfile.chargingSchedule.duration
1210 } is greater than the recurrency time interval duration ${differenceInSeconds(
1211 interval.end,
1212 interval.start
1213 )}`
1214 )
1215 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1216 }
1217 }
1218
1219 const getRandomSerialNumberSuffix = (params?: {
1220 randomBytesLength?: number
1221 upperCase?: boolean
1222 }): string => {
1223 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1224 if (params?.upperCase === true) {
1225 return randomSerialNumberSuffix.toUpperCase()
1226 }
1227 return randomSerialNumberSuffix
1228 }