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