fix: fix TxProfile removal with transaction id defined at Tx stop
[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 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
674 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
675 if (isNotEmptyArray(chargingProfiles)) {
676 const result = getLimitFromChargingProfiles(
677 chargingStation,
678 connectorId,
679 chargingProfiles,
680 chargingStation.logPrefix()
681 )
682 if (result != null) {
683 limit = result.limit
684 chargingProfile = result.chargingProfile
685 switch (chargingStation.stationInfo?.currentOutType) {
686 case CurrentType.AC:
687 limit =
688 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
689 ? limit
690 : ACElectricUtils.powerTotal(
691 chargingStation.getNumberOfPhases(),
692 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
693 chargingStation.stationInfo.voltageOut!,
694 limit
695 )
696 break
697 case CurrentType.DC:
698 limit =
699 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
700 ? limit
701 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
702 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
703 }
704 const connectorMaximumPower =
705 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
706 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
707 if (limit > connectorMaximumPower) {
708 logger.error(
709 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
710 chargingProfile.chargingProfileId
711 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
712 result
713 )
714 limit = connectorMaximumPower
715 }
716 }
717 }
718 return limit
719 }
720
721 export const getDefaultVoltageOut = (
722 currentType: CurrentType,
723 logPrefix: string,
724 templateFile: string
725 ): Voltage => {
726 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
727 let defaultVoltageOut: number
728 switch (currentType) {
729 case CurrentType.AC:
730 defaultVoltageOut = Voltage.VOLTAGE_230
731 break
732 case CurrentType.DC:
733 defaultVoltageOut = Voltage.VOLTAGE_400
734 break
735 default:
736 logger.error(`${logPrefix} ${errorMsg}`)
737 throw new BaseError(errorMsg)
738 }
739 return defaultVoltageOut
740 }
741
742 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
743 return stationInfo.idTagsFile != null
744 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
745 : undefined
746 }
747
748 export const waitChargingStationEvents = async (
749 emitter: EventEmitter,
750 event: ChargingStationWorkerMessageEvents,
751 eventsToWait: number
752 ): Promise<number> => {
753 return await new Promise<number>(resolve => {
754 let events = 0
755 if (eventsToWait === 0) {
756 resolve(events)
757 return
758 }
759 emitter.on(event, () => {
760 ++events
761 if (events === eventsToWait) {
762 resolve(events)
763 }
764 })
765 })
766 }
767
768 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
769 let configuredMaxNumberOfConnectors = 0
770 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
771 const numberOfConnectors = stationTemplate.numberOfConnectors
772 configuredMaxNumberOfConnectors =
773 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
774 } else if (stationTemplate.numberOfConnectors != null) {
775 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
776 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
777 configuredMaxNumberOfConnectors =
778 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
779 stationTemplate.Connectors[0] != null
780 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
781 : getMaxNumberOfConnectors(stationTemplate.Connectors)
782 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
783 for (const evse in stationTemplate.Evses) {
784 if (evse === '0') {
785 continue
786 }
787 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
788 stationTemplate.Evses[evse].Connectors
789 )
790 }
791 }
792 return configuredMaxNumberOfConnectors
793 }
794
795 const checkConfiguredMaxConnectors = (
796 configuredMaxConnectors: number,
797 logPrefix: string,
798 templateFile: string
799 ): void => {
800 if (configuredMaxConnectors <= 0) {
801 logger.warn(
802 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
803 )
804 }
805 }
806
807 const checkTemplateMaxConnectors = (
808 templateMaxConnectors: number,
809 logPrefix: string,
810 templateFile: string
811 ): void => {
812 if (templateMaxConnectors === 0) {
813 logger.warn(
814 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
815 )
816 } else if (templateMaxConnectors < 0) {
817 logger.error(
818 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
819 )
820 }
821 }
822
823 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
824 connectorStatus.availability = AvailabilityType.Operative
825 connectorStatus.idTagLocalAuthorized = false
826 connectorStatus.idTagAuthorized = false
827 connectorStatus.transactionRemoteStarted = false
828 connectorStatus.transactionStarted = false
829 connectorStatus.energyActiveImportRegisterValue = 0
830 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
831 if (connectorStatus.chargingProfiles == null) {
832 connectorStatus.chargingProfiles = []
833 }
834 }
835
836 const warnDeprecatedTemplateKey = (
837 template: ChargingStationTemplate,
838 key: string,
839 logPrefix: string,
840 templateFile: string,
841 logMsgToAppend = ''
842 ): void => {
843 if (template[key as keyof ChargingStationTemplate] != null) {
844 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
845 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
846 }`
847 logger.warn(`${logPrefix} ${logMsg}`)
848 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
849 }
850 }
851
852 const convertDeprecatedTemplateKey = (
853 template: ChargingStationTemplate,
854 deprecatedKey: string,
855 key?: string
856 ): void => {
857 if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
858 if (key != null) {
859 (template as unknown as Record<string, unknown>)[key] =
860 template[deprecatedKey as keyof ChargingStationTemplate]
861 }
862 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
863 delete template[deprecatedKey as keyof ChargingStationTemplate]
864 }
865 }
866
867 interface ChargingProfilesLimit {
868 limit: number
869 chargingProfile: ChargingProfile
870 }
871
872 /**
873 * Charging profiles shall already be sorted by connector id descending then stack level descending
874 *
875 * @param chargingStation -
876 * @param connectorId -
877 * @param chargingProfiles -
878 * @param logPrefix -
879 * @returns ChargingProfilesLimit
880 */
881 const getLimitFromChargingProfiles = (
882 chargingStation: ChargingStation,
883 connectorId: number,
884 chargingProfiles: ChargingProfile[],
885 logPrefix: string
886 ): ChargingProfilesLimit | undefined => {
887 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
888 const currentDate = new Date()
889 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
890 let previousActiveChargingProfile: ChargingProfile | undefined
891 for (const chargingProfile of chargingProfiles) {
892 const chargingSchedule = chargingProfile.chargingSchedule
893 if (chargingSchedule.startSchedule == null) {
894 logger.debug(
895 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
896 )
897 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
898 chargingSchedule.startSchedule = connectorStatus?.transactionStart
899 }
900 if (!isDate(chargingSchedule.startSchedule)) {
901 logger.warn(
902 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
903 )
904 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
905 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
906 }
907 if (chargingSchedule.duration == null) {
908 logger.debug(
909 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
910 )
911 // OCPP specifies that if duration is not defined, it should be infinite
912 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
913 }
914 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
915 continue
916 }
917 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
918 continue
919 }
920 // Check if the charging profile is active
921 if (
922 isWithinInterval(currentDate, {
923 start: chargingSchedule.startSchedule,
924 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
925 })
926 ) {
927 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
928 const chargingSchedulePeriodCompareFn = (
929 a: ChargingSchedulePeriod,
930 b: ChargingSchedulePeriod
931 ): number => a.startPeriod - b.startPeriod
932 if (
933 !isArraySorted<ChargingSchedulePeriod>(
934 chargingSchedule.chargingSchedulePeriod,
935 chargingSchedulePeriodCompareFn
936 )
937 ) {
938 logger.warn(
939 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
940 )
941 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
942 }
943 // Check if the first schedule period startPeriod property is equal to 0
944 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
945 logger.error(
946 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
947 )
948 continue
949 }
950 // Handle only one schedule period
951 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
952 const result: ChargingProfilesLimit = {
953 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
954 chargingProfile
955 }
956 logger.debug(debugLogMsg, result)
957 return result
958 }
959 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
960 // Search for the right schedule period
961 for (const [
962 index,
963 chargingSchedulePeriod
964 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
965 // Find the right schedule period
966 if (
967 isAfter(
968 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
969 currentDate
970 )
971 ) {
972 // Found the schedule period: previous is the correct one
973 const result: ChargingProfilesLimit = {
974 limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
975 chargingProfile: previousActiveChargingProfile ?? chargingProfile
976 }
977 logger.debug(debugLogMsg, result)
978 return result
979 }
980 // Handle the last schedule period within the charging profile duration
981 if (
982 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
983 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
984 differenceInSeconds(
985 addSeconds(
986 chargingSchedule.startSchedule,
987 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
988 ),
989 chargingSchedule.startSchedule
990 ) > chargingSchedule.duration)
991 ) {
992 const result: ChargingProfilesLimit = {
993 limit: chargingSchedulePeriod.limit,
994 chargingProfile
995 }
996 logger.debug(debugLogMsg, result)
997 return result
998 }
999 // Keep a reference to previous charging schedule period
1000 previousChargingSchedulePeriod = chargingSchedulePeriod
1001 }
1002 }
1003 // Keep a reference to previous active charging profile
1004 previousActiveChargingProfile = chargingProfile
1005 }
1006 }
1007 }
1008
1009 export const prepareChargingProfileKind = (
1010 connectorStatus: ConnectorStatus | undefined,
1011 chargingProfile: ChargingProfile,
1012 currentDate: string | number | Date,
1013 logPrefix: string
1014 ): boolean => {
1015 switch (chargingProfile.chargingProfileKind) {
1016 case ChargingProfileKindType.RECURRING:
1017 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
1018 return false
1019 }
1020 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
1021 break
1022 case ChargingProfileKindType.RELATIVE:
1023 if (chargingProfile.chargingSchedule.startSchedule != null) {
1024 logger.warn(
1025 `${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`
1026 )
1027 delete chargingProfile.chargingSchedule.startSchedule
1028 }
1029 if (connectorStatus?.transactionStarted === true) {
1030 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
1031 }
1032 // FIXME: handle relative charging profile duration
1033 break
1034 }
1035 return true
1036 }
1037
1038 export const canProceedChargingProfile = (
1039 chargingProfile: ChargingProfile,
1040 currentDate: string | number | Date,
1041 logPrefix: string
1042 ): boolean => {
1043 if (
1044 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
1045 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
1046 ) {
1047 logger.debug(
1048 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1049 chargingProfile.chargingProfileId
1050 } is not valid for the current date ${
1051 isDate(currentDate) ? currentDate.toISOString() : currentDate
1052 }`
1053 )
1054 return false
1055 }
1056 if (
1057 chargingProfile.chargingSchedule.startSchedule == null ||
1058 chargingProfile.chargingSchedule.duration == null
1059 ) {
1060 logger.error(
1061 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1062 )
1063 return false
1064 }
1065 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
1066 logger.error(
1067 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1068 )
1069 return false
1070 }
1071 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
1072 logger.error(
1073 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1074 )
1075 return false
1076 }
1077 return true
1078 }
1079
1080 const canProceedRecurringChargingProfile = (
1081 chargingProfile: ChargingProfile,
1082 logPrefix: string
1083 ): boolean => {
1084 if (
1085 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1086 chargingProfile.recurrencyKind == null
1087 ) {
1088 logger.error(
1089 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1090 )
1091 return false
1092 }
1093 if (
1094 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1095 chargingProfile.chargingSchedule.startSchedule == null
1096 ) {
1097 logger.error(
1098 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1099 )
1100 return false
1101 }
1102 return true
1103 }
1104
1105 /**
1106 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1107 *
1108 * @param chargingProfile -
1109 * @param currentDate -
1110 * @param logPrefix -
1111 */
1112 const prepareRecurringChargingProfile = (
1113 chargingProfile: ChargingProfile,
1114 currentDate: string | number | Date,
1115 logPrefix: string
1116 ): boolean => {
1117 const chargingSchedule = chargingProfile.chargingSchedule
1118 let recurringIntervalTranslated = false
1119 let recurringInterval: Interval | undefined
1120 switch (chargingProfile.recurrencyKind) {
1121 case RecurrencyKindType.DAILY:
1122 recurringInterval = {
1123 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1124 start: chargingSchedule.startSchedule!,
1125 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1126 end: addDays(chargingSchedule.startSchedule!, 1)
1127 }
1128 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1129 if (
1130 !isWithinInterval(currentDate, recurringInterval) &&
1131 isBefore(recurringInterval.end, currentDate)
1132 ) {
1133 chargingSchedule.startSchedule = addDays(
1134 recurringInterval.start,
1135 differenceInDays(currentDate, recurringInterval.start)
1136 )
1137 recurringInterval = {
1138 start: chargingSchedule.startSchedule,
1139 end: addDays(chargingSchedule.startSchedule, 1)
1140 }
1141 recurringIntervalTranslated = true
1142 }
1143 break
1144 case RecurrencyKindType.WEEKLY:
1145 recurringInterval = {
1146 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1147 start: chargingSchedule.startSchedule!,
1148 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1149 end: addWeeks(chargingSchedule.startSchedule!, 1)
1150 }
1151 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1152 if (
1153 !isWithinInterval(currentDate, recurringInterval) &&
1154 isBefore(recurringInterval.end, currentDate)
1155 ) {
1156 chargingSchedule.startSchedule = addWeeks(
1157 recurringInterval.start,
1158 differenceInWeeks(currentDate, recurringInterval.start)
1159 )
1160 recurringInterval = {
1161 start: chargingSchedule.startSchedule,
1162 end: addWeeks(chargingSchedule.startSchedule, 1)
1163 }
1164 recurringIntervalTranslated = true
1165 }
1166 break
1167 default:
1168 logger.error(
1169 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1170 )
1171 }
1172 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1173 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
1174 logger.error(
1175 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1176 chargingProfile.recurrencyKind
1177 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1178 recurringInterval?.start as Date
1179 ).toISOString()}, ${toDate(
1180 recurringInterval?.end as Date
1181 ).toISOString()}] has not been properly translated to current date ${
1182 isDate(currentDate) ? currentDate.toISOString() : currentDate
1183 } `
1184 )
1185 }
1186 return recurringIntervalTranslated
1187 }
1188
1189 const checkRecurringChargingProfileDuration = (
1190 chargingProfile: ChargingProfile,
1191 interval: Interval,
1192 logPrefix: string
1193 ): void => {
1194 if (chargingProfile.chargingSchedule.duration == null) {
1195 logger.warn(
1196 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1197 chargingProfile.chargingProfileKind
1198 } charging profile id ${
1199 chargingProfile.chargingProfileId
1200 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1201 interval.end,
1202 interval.start
1203 )}`
1204 )
1205 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1206 } else if (
1207 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
1208 ) {
1209 logger.warn(
1210 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1211 chargingProfile.chargingProfileKind
1212 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1213 chargingProfile.chargingSchedule.duration
1214 } is greater than the recurrency time interval duration ${differenceInSeconds(
1215 interval.end,
1216 interval.start
1217 )}`
1218 )
1219 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1220 }
1221 }
1222
1223 const getRandomSerialNumberSuffix = (params?: {
1224 randomBytesLength?: number
1225 upperCase?: boolean
1226 }): string => {
1227 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1228 if (params?.upperCase === true) {
1229 return randomSerialNumberSuffix.toUpperCase()
1230 }
1231 return randomSerialNumberSuffix
1232 }