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