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