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