]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/blob - src/charging-station/Helpers.ts
cadae37bffe002041cdcebe35e54ef21aa7af51b
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
1 import type { EventEmitter } from 'node:events'
2
3 import chalk from 'chalk'
4 import {
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'
19 import { maxTime } from 'date-fns/constants'
20 import { hash, randomBytes } from 'node:crypto'
21 import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path'
22 import { env } from 'node:process'
23 import { fileURLToPath } from 'node:url'
24
25 import type { ChargingStation } from './ChargingStation.js'
26
27 import { BaseError } from '../exception/index.js'
28 import {
29 AmpereUnits,
30 AvailabilityType,
31 type BootNotificationRequest,
32 BootReasonEnumType,
33 type ChargingProfile,
34 ChargingProfileKindType,
35 ChargingProfilePurposeType,
36 ChargingRateUnitType,
37 type ChargingSchedulePeriod,
38 type ChargingStationConfiguration,
39 type ChargingStationInfo,
40 type ChargingStationOptions,
41 type ChargingStationTemplate,
42 type ChargingStationWorkerMessageEvents,
43 ConnectorPhaseRotation,
44 type ConnectorStatus,
45 ConnectorStatusEnum,
46 CurrentType,
47 type EvseTemplate,
48 type OCPP16BootNotificationRequest,
49 type OCPP20BootNotificationRequest,
50 OCPPVersion,
51 RecurrencyKindType,
52 type Reservation,
53 ReservationTerminationReason,
54 StandardParametersKey,
55 type SupportedFeatureProfiles,
56 Voltage,
57 } from '../types/index.js'
58 import {
59 ACElectricUtils,
60 clone,
61 Constants,
62 convertToDate,
63 convertToInt,
64 DCElectricUtils,
65 isArraySorted,
66 isEmpty,
67 isNotEmptyArray,
68 isNotEmptyString,
69 isValidDate,
70 logger,
71 secureRandom,
72 } from '../utils/index.js'
73 import { getConfigurationKey } from './ConfigurationKeyUtils.js'
74
75 const moduleName = 'Helpers'
76
77 export 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
88 export 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
106 export const hasReservationExpired = (reservation: Reservation): boolean => {
107 return isPast(reservation.expiryDate)
108 }
109
110 export 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
142 export 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
157 export 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 hash(
175 Constants.DEFAULT_HASH_ALGORITHM,
176 `${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`,
177 'hex'
178 )
179 }
180
181 export const validateStationInfo = (chargingStation: ChargingStation): void => {
182 if (chargingStation.stationInfo == null || isEmpty(chargingStation.stationInfo)) {
183 throw new BaseError('Missing charging station information')
184 }
185 if (
186 chargingStation.stationInfo.chargingStationId == null ||
187 isEmpty(chargingStation.stationInfo.chargingStationId.trim())
188 ) {
189 throw new BaseError('Missing chargingStationId in stationInfo properties')
190 }
191 const chargingStationId = chargingStation.stationInfo.chargingStationId
192 if (
193 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
194 chargingStation.stationInfo.hashId == null ||
195 isEmpty(chargingStation.stationInfo.hashId.trim())
196 ) {
197 throw new BaseError(`${chargingStationId}: Missing hashId in stationInfo properties`)
198 }
199 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
200 if (chargingStation.stationInfo.templateIndex == null) {
201 throw new BaseError(`${chargingStationId}: Missing templateIndex in stationInfo properties`)
202 }
203 if (chargingStation.stationInfo.templateIndex <= 0) {
204 throw new BaseError(
205 `${chargingStationId}: Invalid templateIndex value in stationInfo properties`
206 )
207 }
208 if (
209 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
210 chargingStation.stationInfo.templateName == null ||
211 isEmpty(chargingStation.stationInfo.templateName.trim())
212 ) {
213 throw new BaseError(`${chargingStationId}: Missing templateName in stationInfo properties`)
214 }
215 if (chargingStation.stationInfo.maximumPower == null) {
216 throw new BaseError(`${chargingStationId}: Missing maximumPower in stationInfo properties`)
217 }
218 if (chargingStation.stationInfo.maximumPower <= 0) {
219 throw new RangeError(
220 `${chargingStationId}: Invalid maximumPower value in stationInfo properties`
221 )
222 }
223 if (chargingStation.stationInfo.maximumAmperage == null) {
224 throw new BaseError(`${chargingStationId}: Missing maximumAmperage in stationInfo properties`)
225 }
226 if (chargingStation.stationInfo.maximumAmperage <= 0) {
227 throw new RangeError(
228 `${chargingStationId}: Invalid maximumAmperage value in stationInfo properties`
229 )
230 }
231 switch (chargingStation.stationInfo.ocppVersion) {
232 case OCPPVersion.VERSION_20:
233 case OCPPVersion.VERSION_201:
234 if (isEmpty(chargingStation.evses)) {
235 throw new BaseError(
236 `${chargingStationId}: OCPP 2.0 or superior requires at least one EVSE defined in the charging station template/configuration`
237 )
238 }
239 }
240 }
241
242 export const checkChargingStationState = (
243 chargingStation: ChargingStation,
244 logPrefix: string
245 ): boolean => {
246 if (!chargingStation.started && !chargingStation.starting) {
247 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`)
248 return false
249 }
250 return true
251 }
252
253 export const getPhaseRotationValue = (
254 connectorId: number,
255 numberOfPhases: number
256 ): string | undefined => {
257 // AC/DC
258 if (connectorId === 0 && numberOfPhases === 0) {
259 return `${connectorId.toString()}.${ConnectorPhaseRotation.RST}`
260 } else if (connectorId > 0 && numberOfPhases === 0) {
261 return `${connectorId.toString()}.${ConnectorPhaseRotation.NotApplicable}`
262 // AC
263 } else if (connectorId >= 0 && numberOfPhases === 1) {
264 return `${connectorId.toString()}.${ConnectorPhaseRotation.NotApplicable}`
265 } else if (connectorId >= 0 && numberOfPhases === 3) {
266 return `${connectorId.toString()}.${ConnectorPhaseRotation.RST}`
267 }
268 }
269
270 export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate> | undefined): number => {
271 if (evses == null) {
272 return -1
273 }
274 return Object.keys(evses).length
275 }
276
277 const getMaxNumberOfConnectors = (
278 connectors: Record<string, ConnectorStatus> | undefined
279 ): number => {
280 if (connectors == null) {
281 return -1
282 }
283 return Object.keys(connectors).length
284 }
285
286 export const getBootConnectorStatus = (
287 chargingStation: ChargingStation,
288 connectorId: number,
289 connectorStatus: ConnectorStatus
290 ): ConnectorStatusEnum => {
291 let connectorBootStatus: ConnectorStatusEnum
292 if (
293 connectorStatus.status == null &&
294 (!chargingStation.isChargingStationAvailable() ||
295 !chargingStation.isConnectorAvailable(connectorId))
296 ) {
297 connectorBootStatus = ConnectorStatusEnum.Unavailable
298 } else if (connectorStatus.status == null && connectorStatus.bootStatus != null) {
299 // Set boot status in template at startup
300 connectorBootStatus = connectorStatus.bootStatus
301 } else if (connectorStatus.status != null) {
302 // Set previous status at startup
303 connectorBootStatus = connectorStatus.status
304 } else {
305 // Set default status
306 connectorBootStatus = ConnectorStatusEnum.Available
307 }
308 return connectorBootStatus
309 }
310
311 export const checkTemplate = (
312 stationTemplate: ChargingStationTemplate | undefined,
313 logPrefix: string,
314 templateFile: string
315 ): void => {
316 if (stationTemplate == null) {
317 const errorMsg = `Failed to read charging station template file ${templateFile}`
318 logger.error(`${logPrefix} ${errorMsg}`)
319 throw new BaseError(errorMsg)
320 }
321 if (isEmpty(stationTemplate)) {
322 const errorMsg = `Empty charging station information from template file ${templateFile}`
323 logger.error(`${logPrefix} ${errorMsg}`)
324 throw new BaseError(errorMsg)
325 }
326 if (stationTemplate.idTagsFile == null || isEmpty(stationTemplate.idTagsFile)) {
327 logger.warn(
328 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
329 )
330 }
331 }
332
333 export const checkConfiguration = (
334 stationConfiguration: ChargingStationConfiguration | undefined,
335 logPrefix: string,
336 configurationFile: string
337 ): void => {
338 if (stationConfiguration == null) {
339 const errorMsg = `Failed to read charging station configuration file ${configurationFile}`
340 logger.error(`${logPrefix} ${errorMsg}`)
341 throw new BaseError(errorMsg)
342 }
343 if (isEmpty(stationConfiguration)) {
344 const errorMsg = `Empty charging station configuration from file ${configurationFile}`
345 logger.error(`${logPrefix} ${errorMsg}`)
346 throw new BaseError(errorMsg)
347 }
348 }
349
350 export const checkConnectorsConfiguration = (
351 stationTemplate: ChargingStationTemplate,
352 logPrefix: string,
353 templateFile: string
354 ): {
355 configuredMaxConnectors: number
356 templateMaxAvailableConnectors: number
357 templateMaxConnectors: number
358 } => {
359 const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate)
360 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile)
361 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors)
362 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile)
363 const templateMaxAvailableConnectors =
364 stationTemplate.Connectors?.[0] != null ? templateMaxConnectors - 1 : templateMaxConnectors
365 if (
366 configuredMaxConnectors > templateMaxAvailableConnectors &&
367 stationTemplate.randomConnectors !== true
368 ) {
369 logger.warn(
370 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
371 )
372 stationTemplate.randomConnectors = true
373 }
374 return {
375 configuredMaxConnectors,
376 templateMaxAvailableConnectors,
377 templateMaxConnectors,
378 }
379 }
380
381 export const checkStationInfoConnectorStatus = (
382 connectorId: number,
383 connectorStatus: ConnectorStatus,
384 logPrefix: string,
385 templateFile: string
386 ): void => {
387 if (connectorStatus.status != null) {
388 logger.warn(
389 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId.toString()} status configuration defined, undefine it`
390 )
391 delete connectorStatus.status
392 }
393 }
394
395 export const setChargingStationOptions = (
396 stationInfo: ChargingStationInfo,
397 options?: ChargingStationOptions
398 ): ChargingStationInfo => {
399 if (options?.supervisionUrls != null) {
400 stationInfo.supervisionUrls = options.supervisionUrls
401 }
402 if (options?.persistentConfiguration != null) {
403 stationInfo.stationInfoPersistentConfiguration = options.persistentConfiguration
404 stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
405 stationInfo.automaticTransactionGeneratorPersistentConfiguration =
406 options.persistentConfiguration
407 }
408 if (options?.autoStart != null) {
409 stationInfo.autoStart = options.autoStart
410 }
411 if (options?.autoRegister != null) {
412 stationInfo.autoRegister = options.autoRegister
413 }
414 if (options?.enableStatistics != null) {
415 stationInfo.enableStatistics = options.enableStatistics
416 }
417 if (options?.ocppStrictCompliance != null) {
418 stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
419 }
420 if (options?.stopTransactionsOnStopped != null) {
421 stationInfo.stopTransactionsOnStopped = options.stopTransactionsOnStopped
422 }
423 return stationInfo
424 }
425
426 export const buildConnectorsMap = (
427 connectors: Record<string, ConnectorStatus>,
428 logPrefix: string,
429 templateFile: string
430 ): Map<number, ConnectorStatus> => {
431 const connectorsMap = new Map<number, ConnectorStatus>()
432 if (getMaxNumberOfConnectors(connectors) > 0) {
433 for (const connector in connectors) {
434 const connectorStatus = connectors[connector]
435 const connectorId = convertToInt(connector)
436 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
437 connectorsMap.set(connectorId, clone<ConnectorStatus>(connectorStatus))
438 }
439 } else {
440 logger.warn(
441 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
442 )
443 }
444 return connectorsMap
445 }
446
447 export const initializeConnectorsMapStatus = (
448 connectors: Map<number, ConnectorStatus>,
449 logPrefix: string
450 ): void => {
451 for (const connectorId of connectors.keys()) {
452 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
453 logger.warn(
454 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
455 `${logPrefix} Connector id ${connectorId.toString()} at initialization has a transaction started with id ${connectors
456 .get(connectorId)
457 ?.transactionId?.toString()}`
458 )
459 }
460 if (connectorId === 0) {
461 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
462 connectors.get(connectorId)!.availability = AvailabilityType.Operative
463 if (connectors.get(connectorId)?.chargingProfiles == null) {
464 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
465 connectors.get(connectorId)!.chargingProfiles = []
466 }
467 } else if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted == null) {
468 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
469 initializeConnectorStatus(connectors.get(connectorId)!)
470 }
471 }
472 }
473
474 export const resetAuthorizeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
475 connectorStatus.idTagLocalAuthorized = false
476 connectorStatus.idTagAuthorized = false
477 delete connectorStatus.localAuthorizeIdTag
478 delete connectorStatus.authorizeIdTag
479 }
480
481 export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefined): void => {
482 if (connectorStatus == null) {
483 return
484 }
485 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
486 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles.filter(
487 chargingProfile =>
488 (chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE &&
489 chargingProfile.transactionId != null &&
490 connectorStatus.transactionId != null &&
491 chargingProfile.transactionId !== connectorStatus.transactionId) ||
492 chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE
493 )
494 }
495 resetAuthorizeConnectorStatus(connectorStatus)
496 connectorStatus.transactionRemoteStarted = false
497 connectorStatus.transactionStarted = false
498 delete connectorStatus.transactionStart
499 delete connectorStatus.transactionId
500 delete connectorStatus.transactionIdTag
501 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
502 delete connectorStatus.transactionBeginMeterValue
503 }
504
505 export const prepareConnectorStatus = (connectorStatus: ConnectorStatus): ConnectorStatus => {
506 if (connectorStatus.reservation != null) {
507 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
508 connectorStatus.reservation.expiryDate = convertToDate(connectorStatus.reservation.expiryDate)!
509 }
510 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
511 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles
512 .filter(
513 chargingProfile =>
514 chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE
515 )
516 .map(chargingProfile => {
517 chargingProfile.chargingSchedule.startSchedule = convertToDate(
518 chargingProfile.chargingSchedule.startSchedule
519 )
520 chargingProfile.validFrom = convertToDate(chargingProfile.validFrom)
521 chargingProfile.validTo = convertToDate(chargingProfile.validTo)
522 return chargingProfile
523 })
524 }
525 return connectorStatus
526 }
527
528 export const createBootNotificationRequest = (
529 stationInfo: ChargingStationInfo,
530 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
531 ): BootNotificationRequest | undefined => {
532 const ocppVersion = stationInfo.ocppVersion
533 switch (ocppVersion) {
534 case OCPPVersion.VERSION_16:
535 return {
536 chargePointModel: stationInfo.chargePointModel,
537 chargePointVendor: stationInfo.chargePointVendor,
538 ...(stationInfo.chargeBoxSerialNumber != null && {
539 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
540 }),
541 ...(stationInfo.chargePointSerialNumber != null && {
542 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
543 }),
544 ...(stationInfo.firmwareVersion != null && {
545 firmwareVersion: stationInfo.firmwareVersion,
546 }),
547 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
548 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
549 ...(stationInfo.meterSerialNumber != null && {
550 meterSerialNumber: stationInfo.meterSerialNumber,
551 }),
552 ...(stationInfo.meterType != null && {
553 meterType: stationInfo.meterType,
554 }),
555 } satisfies OCPP16BootNotificationRequest
556 case OCPPVersion.VERSION_20:
557 case OCPPVersion.VERSION_201:
558 return {
559 chargingStation: {
560 model: stationInfo.chargePointModel,
561 vendorName: stationInfo.chargePointVendor,
562 ...(stationInfo.firmwareVersion != null && {
563 firmwareVersion: stationInfo.firmwareVersion,
564 }),
565 ...(stationInfo.chargeBoxSerialNumber != null && {
566 serialNumber: stationInfo.chargeBoxSerialNumber,
567 }),
568 ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
569 modem: {
570 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
571 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
572 },
573 }),
574 },
575 reason: bootReason,
576 } satisfies OCPP20BootNotificationRequest
577 }
578 }
579
580 export const warnTemplateKeysDeprecation = (
581 stationTemplate: ChargingStationTemplate,
582 logPrefix: string,
583 templateFile: string
584 ): void => {
585 const templateKeys: { deprecatedKey: string; key?: string }[] = [
586 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
587 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
588 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
589 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' },
590 ]
591 for (const templateKey of templateKeys) {
592 warnDeprecatedTemplateKey(
593 stationTemplate,
594 templateKey.deprecatedKey,
595 logPrefix,
596 templateFile,
597 templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
598 )
599 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
600 }
601 }
602
603 export const stationTemplateToStationInfo = (
604 stationTemplate: ChargingStationTemplate
605 ): ChargingStationInfo => {
606 stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
607 delete stationTemplate.power
608 delete stationTemplate.powerUnit
609 delete stationTemplate.Connectors
610 delete stationTemplate.Evses
611 delete stationTemplate.Configuration
612 delete stationTemplate.AutomaticTransactionGenerator
613 delete stationTemplate.numberOfConnectors
614 delete stationTemplate.chargeBoxSerialNumberPrefix
615 delete stationTemplate.chargePointSerialNumberPrefix
616 delete stationTemplate.meterSerialNumberPrefix
617 return stationTemplate as ChargingStationInfo
618 }
619
620 export const createSerialNumber = (
621 stationTemplate: ChargingStationTemplate,
622 stationInfo: ChargingStationInfo,
623 params?: {
624 randomSerialNumber?: boolean
625 randomSerialNumberUpperCase?: boolean
626 }
627 ): void => {
628 params = {
629 ...{ randomSerialNumber: true, randomSerialNumberUpperCase: true },
630 ...params,
631 }
632 const serialNumberSuffix = params.randomSerialNumber
633 ? getRandomSerialNumberSuffix({
634 upperCase: params.randomSerialNumberUpperCase,
635 })
636 : ''
637 isNotEmptyString(stationTemplate.chargePointSerialNumberPrefix) &&
638 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
639 isNotEmptyString(stationTemplate.chargeBoxSerialNumberPrefix) &&
640 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
641 isNotEmptyString(stationTemplate.meterSerialNumberPrefix) &&
642 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
643 }
644
645 export const propagateSerialNumber = (
646 stationTemplate: ChargingStationTemplate | undefined,
647 stationInfoSrc: ChargingStationInfo | undefined,
648 stationInfoDst: ChargingStationInfo
649 ): void => {
650 if (stationInfoSrc == null || stationTemplate == null) {
651 throw new BaseError(
652 'Missing charging station template or existing configuration to propagate serial number'
653 )
654 }
655 stationTemplate.chargePointSerialNumberPrefix != null &&
656 stationInfoSrc.chargePointSerialNumber != null
657 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
658 : stationInfoDst.chargePointSerialNumber != null &&
659 delete stationInfoDst.chargePointSerialNumber
660 stationTemplate.chargeBoxSerialNumberPrefix != null &&
661 stationInfoSrc.chargeBoxSerialNumber != null
662 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
663 : stationInfoDst.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
664 stationTemplate.meterSerialNumberPrefix != null && stationInfoSrc.meterSerialNumber != null
665 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
666 : stationInfoDst.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
667 }
668
669 export const hasFeatureProfile = (
670 chargingStation: ChargingStation,
671 featureProfile: SupportedFeatureProfiles
672 ): boolean | undefined => {
673 return getConfigurationKey(
674 chargingStation,
675 StandardParametersKey.SupportedFeatureProfiles
676 )?.value?.includes(featureProfile)
677 }
678
679 export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
680 let unitDivider = 1
681 switch (stationInfo.amperageLimitationUnit) {
682 case AmpereUnits.CENTI_AMPERE:
683 unitDivider = 100
684 break
685 case AmpereUnits.DECI_AMPERE:
686 unitDivider = 10
687 break
688 case AmpereUnits.MILLI_AMPERE:
689 unitDivider = 1000
690 break
691 }
692 return unitDivider
693 }
694
695 const getChargingStationChargingProfiles = (
696 chargingStation: ChargingStation
697 ): ChargingProfile[] => {
698 return (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [])
699 .filter(
700 chargingProfile =>
701 chargingProfile.chargingProfilePurpose ===
702 ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE
703 )
704 .sort((a, b) => b.stackLevel - a.stackLevel)
705 }
706
707 export const getChargingStationChargingProfilesLimit = (
708 chargingStation: ChargingStation
709 ): number | undefined => {
710 const chargingProfiles = getChargingStationChargingProfiles(chargingStation)
711 if (isNotEmptyArray(chargingProfiles)) {
712 const chargingProfilesLimit = getChargingProfilesLimit(chargingStation, 0, chargingProfiles)
713 if (chargingProfilesLimit != null) {
714 const limit = buildChargingProfilesLimit(chargingStation, chargingProfilesLimit)
715 const chargingStationMaximumPower =
716 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
717 chargingStation.stationInfo!.maximumPower!
718 if (limit > chargingStationMaximumPower) {
719 logger.error(
720 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationChargingProfilesLimit: Charging profile id ${chargingProfilesLimit.chargingProfile.chargingProfileId.toString()} limit ${limit.toString()} is greater than charging station maximum ${chargingStationMaximumPower.toString()}: %j`,
721 chargingProfilesLimit
722 )
723 return chargingStationMaximumPower
724 }
725 return limit
726 }
727 }
728 }
729
730 /**
731 * Gets the connector charging profiles relevant for power limitation shallow cloned
732 * and sorted by priorities
733 * @param chargingStation - Charging station
734 * @param connectorId - Connector id
735 * @returns connector charging profiles array
736 */
737 export const getConnectorChargingProfiles = (
738 chargingStation: ChargingStation,
739 connectorId: number
740 ): ChargingProfile[] => {
741 return (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
742 .slice()
743 .sort((a, b) => {
744 if (
745 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE &&
746 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
747 ) {
748 return -1
749 } else if (
750 a.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE &&
751 b.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE
752 ) {
753 return 1
754 }
755 return b.stackLevel - a.stackLevel
756 })
757 .concat(
758 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [])
759 .filter(
760 chargingProfile =>
761 chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_DEFAULT_PROFILE
762 )
763 .sort((a, b) => b.stackLevel - a.stackLevel)
764 )
765 }
766
767 export const getConnectorChargingProfilesLimit = (
768 chargingStation: ChargingStation,
769 connectorId: number
770 ): number | undefined => {
771 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
772 if (isNotEmptyArray(chargingProfiles)) {
773 const chargingProfilesLimit = getChargingProfilesLimit(
774 chargingStation,
775 connectorId,
776 chargingProfiles
777 )
778 if (chargingProfilesLimit != null) {
779 const limit = buildChargingProfilesLimit(chargingStation, chargingProfilesLimit)
780 const connectorMaximumPower =
781 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
782 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
783 if (limit > connectorMaximumPower) {
784 logger.error(
785 `${chargingStation.logPrefix()} ${moduleName}.getConnectorChargingProfilesLimit: Charging profile id ${chargingProfilesLimit.chargingProfile.chargingProfileId.toString()} limit ${limit.toString()} is greater than connector ${connectorId.toString()} maximum ${connectorMaximumPower.toString()}: %j`,
786 chargingProfilesLimit
787 )
788 return connectorMaximumPower
789 }
790 return limit
791 }
792 }
793 }
794
795 const buildChargingProfilesLimit = (
796 chargingStation: ChargingStation,
797 chargingProfilesLimit: ChargingProfilesLimit
798 ): number => {
799 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
800 const errorMsg = `Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in charging station information, cannot build charging profiles limit`
801 const { chargingProfile, limit } = chargingProfilesLimit
802 switch (chargingStation.stationInfo?.currentOutType) {
803 case CurrentType.AC:
804 return chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
805 ? limit
806 : ACElectricUtils.powerTotal(
807 chargingStation.getNumberOfPhases(),
808 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
809 chargingStation.stationInfo.voltageOut!,
810 limit
811 )
812 case CurrentType.DC:
813 return chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
814 ? limit
815 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
816 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
817 default:
818 logger.error(
819 `${chargingStation.logPrefix()} ${moduleName}.buildChargingProfilesLimit: ${errorMsg}`
820 )
821 throw new BaseError(errorMsg)
822 }
823 }
824
825 export const getDefaultVoltageOut = (
826 currentType: CurrentType,
827 logPrefix: string,
828 templateFile: string
829 ): Voltage => {
830 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
831 let defaultVoltageOut: number
832 switch (currentType) {
833 case CurrentType.AC:
834 defaultVoltageOut = Voltage.VOLTAGE_230
835 break
836 case CurrentType.DC:
837 defaultVoltageOut = Voltage.VOLTAGE_400
838 break
839 default:
840 logger.error(`${logPrefix} ${errorMsg}`)
841 throw new BaseError(errorMsg)
842 }
843 return defaultVoltageOut
844 }
845
846 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
847 return stationInfo.idTagsFile != null
848 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
849 : undefined
850 }
851
852 export const waitChargingStationEvents = async (
853 emitter: EventEmitter,
854 event: ChargingStationWorkerMessageEvents,
855 eventsToWait: number
856 ): Promise<number> => {
857 return await new Promise<number>(resolve => {
858 let events = 0
859 if (eventsToWait === 0) {
860 resolve(events)
861 return
862 }
863 emitter.on(event, () => {
864 ++events
865 if (events === eventsToWait) {
866 resolve(events)
867 }
868 })
869 })
870 }
871
872 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
873 let configuredMaxNumberOfConnectors = 0
874 if (isNotEmptyArray<number>(stationTemplate.numberOfConnectors)) {
875 const numberOfConnectors = stationTemplate.numberOfConnectors
876 configuredMaxNumberOfConnectors =
877 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
878 } else if (typeof stationTemplate.numberOfConnectors === 'number') {
879 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
880 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
881 configuredMaxNumberOfConnectors =
882 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
883 stationTemplate.Connectors[0] != null
884 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
885 : getMaxNumberOfConnectors(stationTemplate.Connectors)
886 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
887 for (const evse in stationTemplate.Evses) {
888 if (evse === '0') {
889 continue
890 }
891 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
892 stationTemplate.Evses[evse].Connectors
893 )
894 }
895 }
896 return configuredMaxNumberOfConnectors
897 }
898
899 const checkConfiguredMaxConnectors = (
900 configuredMaxConnectors: number,
901 logPrefix: string,
902 templateFile: string
903 ): void => {
904 if (configuredMaxConnectors <= 0) {
905 logger.warn(
906 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors.toString()} connectors`
907 )
908 }
909 }
910
911 const checkTemplateMaxConnectors = (
912 templateMaxConnectors: number,
913 logPrefix: string,
914 templateFile: string
915 ): void => {
916 if (templateMaxConnectors === 0) {
917 logger.warn(
918 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
919 )
920 } else if (templateMaxConnectors < 0) {
921 logger.error(
922 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
923 )
924 }
925 }
926
927 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
928 connectorStatus.availability = AvailabilityType.Operative
929 connectorStatus.idTagLocalAuthorized = false
930 connectorStatus.idTagAuthorized = false
931 connectorStatus.transactionRemoteStarted = false
932 connectorStatus.transactionStarted = false
933 connectorStatus.energyActiveImportRegisterValue = 0
934 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
935 connectorStatus.chargingProfiles ??= []
936 }
937
938 const 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
954 const 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
969 interface 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 */
982 const 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
1116 export 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
1145 export 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
1185 const 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 */
1217 const 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
1296 const 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
1326 const 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 }