refactor: factor out more charging station options handling code
[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 != null && {
150 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix
151 }),
152 ...(stationTemplate.chargePointSerialNumberPrefix != null && {
153 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
154 }),
155 ...(stationTemplate.meterSerialNumberPrefix != null && {
156 meterSerialNumber: stationTemplate.meterSerialNumberPrefix
157 }),
158 ...(stationTemplate.meterType != null && {
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.stationInfoPersistentConfiguration = options.persistentConfiguration
343 stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
344 stationInfo.automaticTransactionGeneratorPersistentConfiguration =
345 options.persistentConfiguration
346 }
347 if (options?.autoStart != null) {
348 stationInfo.autoStart = options.autoStart
349 }
350 if (options?.autoRegister != null) {
351 stationInfo.autoRegister = options.autoRegister
352 }
353 if (options?.enableStatistics != null) {
354 stationInfo.enableStatistics = options.enableStatistics
355 }
356 if (options?.ocppStrictCompliance != null) {
357 stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
358 }
359 if (options?.stopTransactionsOnStopped != null) {
360 stationInfo.stopTransactionsOnStopped = options.stopTransactionsOnStopped
361 }
362 return stationInfo
363 }
364
365 export const initializeConnectorsMapStatus = (
366 connectors: Map<number, ConnectorStatus>,
367 logPrefix: string
368 ): void => {
369 for (const connectorId of connectors.keys()) {
370 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
371 logger.warn(
372 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
373 connectors.get(connectorId)?.transactionId
374 }`
375 )
376 }
377 if (connectorId === 0) {
378 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
379 connectors.get(connectorId)!.availability = AvailabilityType.Operative
380 if (connectors.get(connectorId)?.chargingProfiles == null) {
381 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
382 connectors.get(connectorId)!.chargingProfiles = []
383 }
384 } else if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted == null) {
385 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
386 initializeConnectorStatus(connectors.get(connectorId)!)
387 }
388 }
389 }
390
391 export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefined): void => {
392 if (connectorStatus == null) {
393 return
394 }
395 connectorStatus.chargingProfiles =
396 connectorStatus.transactionId != null && isNotEmptyArray(connectorStatus.chargingProfiles)
397 ? connectorStatus.chargingProfiles.filter(
398 chargingProfile => chargingProfile.transactionId !== connectorStatus.transactionId
399 )
400 : []
401 connectorStatus.idTagLocalAuthorized = false
402 connectorStatus.idTagAuthorized = false
403 connectorStatus.transactionRemoteStarted = false
404 connectorStatus.transactionStarted = false
405 delete connectorStatus.transactionStart
406 delete connectorStatus.transactionId
407 delete connectorStatus.localAuthorizeIdTag
408 delete connectorStatus.authorizeIdTag
409 delete connectorStatus.transactionIdTag
410 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
411 delete connectorStatus.transactionBeginMeterValue
412 }
413
414 export const createBootNotificationRequest = (
415 stationInfo: ChargingStationInfo,
416 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
417 ): BootNotificationRequest | undefined => {
418 const ocppVersion = stationInfo.ocppVersion
419 switch (ocppVersion) {
420 case OCPPVersion.VERSION_16:
421 return {
422 chargePointModel: stationInfo.chargePointModel,
423 chargePointVendor: stationInfo.chargePointVendor,
424 ...(stationInfo.chargeBoxSerialNumber != null && {
425 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
426 }),
427 ...(stationInfo.chargePointSerialNumber != null && {
428 chargePointSerialNumber: stationInfo.chargePointSerialNumber
429 }),
430 ...(stationInfo.firmwareVersion != null && {
431 firmwareVersion: stationInfo.firmwareVersion
432 }),
433 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
434 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
435 ...(stationInfo.meterSerialNumber != null && {
436 meterSerialNumber: stationInfo.meterSerialNumber
437 }),
438 ...(stationInfo.meterType != null && {
439 meterType: stationInfo.meterType
440 })
441 } satisfies OCPP16BootNotificationRequest
442 case OCPPVersion.VERSION_20:
443 case OCPPVersion.VERSION_201:
444 return {
445 reason: bootReason,
446 chargingStation: {
447 model: stationInfo.chargePointModel,
448 vendorName: stationInfo.chargePointVendor,
449 ...(stationInfo.firmwareVersion != null && {
450 firmwareVersion: stationInfo.firmwareVersion
451 }),
452 ...(stationInfo.chargeBoxSerialNumber != null && {
453 serialNumber: stationInfo.chargeBoxSerialNumber
454 }),
455 ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
456 modem: {
457 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
458 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi })
459 }
460 })
461 }
462 } satisfies OCPP20BootNotificationRequest
463 }
464 }
465
466 export const warnTemplateKeysDeprecation = (
467 stationTemplate: ChargingStationTemplate,
468 logPrefix: string,
469 templateFile: string
470 ): void => {
471 const templateKeys: Array<{ deprecatedKey: string, key?: string }> = [
472 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
473 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
474 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
475 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' }
476 ]
477 for (const templateKey of templateKeys) {
478 warnDeprecatedTemplateKey(
479 stationTemplate,
480 templateKey.deprecatedKey,
481 logPrefix,
482 templateFile,
483 templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
484 )
485 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
486 }
487 }
488
489 export const stationTemplateToStationInfo = (
490 stationTemplate: ChargingStationTemplate
491 ): ChargingStationInfo => {
492 stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
493 delete stationTemplate.power
494 delete stationTemplate.powerUnit
495 delete stationTemplate.Connectors
496 delete stationTemplate.Evses
497 delete stationTemplate.Configuration
498 delete stationTemplate.AutomaticTransactionGenerator
499 delete stationTemplate.chargeBoxSerialNumberPrefix
500 delete stationTemplate.chargePointSerialNumberPrefix
501 delete stationTemplate.meterSerialNumberPrefix
502 return stationTemplate as ChargingStationInfo
503 }
504
505 export const createSerialNumber = (
506 stationTemplate: ChargingStationTemplate,
507 stationInfo: ChargingStationInfo,
508 params?: {
509 randomSerialNumberUpperCase?: boolean
510 randomSerialNumber?: boolean
511 }
512 ): void => {
513 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params }
514 const serialNumberSuffix =
515 params.randomSerialNumber === true
516 ? getRandomSerialNumberSuffix({
517 upperCase: params.randomSerialNumberUpperCase
518 })
519 : ''
520 isNotEmptyString(stationTemplate.chargePointSerialNumberPrefix) &&
521 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
522 isNotEmptyString(stationTemplate.chargeBoxSerialNumberPrefix) &&
523 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
524 isNotEmptyString(stationTemplate.meterSerialNumberPrefix) &&
525 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
526 }
527
528 export const propagateSerialNumber = (
529 stationTemplate: ChargingStationTemplate | undefined,
530 stationInfoSrc: ChargingStationInfo | undefined,
531 stationInfoDst: ChargingStationInfo
532 ): void => {
533 if (stationInfoSrc == null || stationTemplate == null) {
534 throw new BaseError(
535 'Missing charging station template or existing configuration to propagate serial number'
536 )
537 }
538 stationTemplate.chargePointSerialNumberPrefix != null &&
539 stationInfoSrc.chargePointSerialNumber != null
540 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
541 : stationInfoDst.chargePointSerialNumber != null &&
542 delete stationInfoDst.chargePointSerialNumber
543 stationTemplate.chargeBoxSerialNumberPrefix != null &&
544 stationInfoSrc.chargeBoxSerialNumber != null
545 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
546 : stationInfoDst.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
547 stationTemplate.meterSerialNumberPrefix != null && stationInfoSrc.meterSerialNumber != null
548 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
549 : stationInfoDst.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
550 }
551
552 export const hasFeatureProfile = (
553 chargingStation: ChargingStation,
554 featureProfile: SupportedFeatureProfiles
555 ): boolean | undefined => {
556 return getConfigurationKey(
557 chargingStation,
558 StandardParametersKey.SupportedFeatureProfiles
559 )?.value?.includes(featureProfile)
560 }
561
562 export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
563 let unitDivider = 1
564 switch (stationInfo.amperageLimitationUnit) {
565 case AmpereUnits.DECI_AMPERE:
566 unitDivider = 10
567 break
568 case AmpereUnits.CENTI_AMPERE:
569 unitDivider = 100
570 break
571 case AmpereUnits.MILLI_AMPERE:
572 unitDivider = 1000
573 break
574 }
575 return unitDivider
576 }
577
578 /**
579 * Gets the connector cloned charging profiles applying a power limitation
580 * and sorted by connector id descending then stack level descending
581 *
582 * @param chargingStation -
583 * @param connectorId -
584 * @returns connector charging profiles array
585 */
586 export const getConnectorChargingProfiles = (
587 chargingStation: ChargingStation,
588 connectorId: number
589 ): ChargingProfile[] => {
590 return clone<ChargingProfile[]>(
591 (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
592 .sort((a, b) => b.stackLevel - a.stackLevel)
593 .concat(
594 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
595 (a, b) => b.stackLevel - a.stackLevel
596 )
597 )
598 )
599 }
600
601 export const getChargingStationConnectorChargingProfilesPowerLimit = (
602 chargingStation: ChargingStation,
603 connectorId: number
604 ): number | undefined => {
605 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
606 // Get charging profiles sorted by connector id then stack level
607 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
608 if (isNotEmptyArray(chargingProfiles)) {
609 const result = getLimitFromChargingProfiles(
610 chargingStation,
611 connectorId,
612 chargingProfiles,
613 chargingStation.logPrefix()
614 )
615 if (result != null) {
616 limit = result.limit
617 chargingProfile = result.chargingProfile
618 switch (chargingStation.stationInfo?.currentOutType) {
619 case CurrentType.AC:
620 limit =
621 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
622 ? limit
623 : ACElectricUtils.powerTotal(
624 chargingStation.getNumberOfPhases(),
625 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
626 chargingStation.stationInfo.voltageOut!,
627 limit
628 )
629 break
630 case CurrentType.DC:
631 limit =
632 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
633 ? limit
634 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
635 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
636 }
637 const connectorMaximumPower =
638 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
639 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
640 if (limit > connectorMaximumPower) {
641 logger.error(
642 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
643 chargingProfile.chargingProfileId
644 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
645 result
646 )
647 limit = connectorMaximumPower
648 }
649 }
650 }
651 return limit
652 }
653
654 export const getDefaultVoltageOut = (
655 currentType: CurrentType,
656 logPrefix: string,
657 templateFile: string
658 ): Voltage => {
659 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
660 let defaultVoltageOut: number
661 switch (currentType) {
662 case CurrentType.AC:
663 defaultVoltageOut = Voltage.VOLTAGE_230
664 break
665 case CurrentType.DC:
666 defaultVoltageOut = Voltage.VOLTAGE_400
667 break
668 default:
669 logger.error(`${logPrefix} ${errorMsg}`)
670 throw new BaseError(errorMsg)
671 }
672 return defaultVoltageOut
673 }
674
675 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
676 return stationInfo.idTagsFile != null
677 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
678 : undefined
679 }
680
681 export const waitChargingStationEvents = async (
682 emitter: EventEmitter,
683 event: ChargingStationWorkerMessageEvents,
684 eventsToWait: number
685 ): Promise<number> => {
686 return await new Promise<number>(resolve => {
687 let events = 0
688 if (eventsToWait === 0) {
689 resolve(events)
690 return
691 }
692 emitter.on(event, () => {
693 ++events
694 if (events === eventsToWait) {
695 resolve(events)
696 }
697 })
698 })
699 }
700
701 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
702 let configuredMaxNumberOfConnectors = 0
703 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
704 const numberOfConnectors = stationTemplate.numberOfConnectors
705 configuredMaxNumberOfConnectors =
706 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
707 } else if (stationTemplate.numberOfConnectors != null) {
708 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
709 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
710 configuredMaxNumberOfConnectors =
711 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
712 stationTemplate.Connectors[0] != null
713 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
714 : getMaxNumberOfConnectors(stationTemplate.Connectors)
715 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
716 for (const evse in stationTemplate.Evses) {
717 if (evse === '0') {
718 continue
719 }
720 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
721 stationTemplate.Evses[evse].Connectors
722 )
723 }
724 }
725 return configuredMaxNumberOfConnectors
726 }
727
728 const checkConfiguredMaxConnectors = (
729 configuredMaxConnectors: number,
730 logPrefix: string,
731 templateFile: string
732 ): void => {
733 if (configuredMaxConnectors <= 0) {
734 logger.warn(
735 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
736 )
737 }
738 }
739
740 const checkTemplateMaxConnectors = (
741 templateMaxConnectors: number,
742 logPrefix: string,
743 templateFile: string
744 ): void => {
745 if (templateMaxConnectors === 0) {
746 logger.warn(
747 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
748 )
749 } else if (templateMaxConnectors < 0) {
750 logger.error(
751 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
752 )
753 }
754 }
755
756 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
757 connectorStatus.availability = AvailabilityType.Operative
758 connectorStatus.idTagLocalAuthorized = false
759 connectorStatus.idTagAuthorized = false
760 connectorStatus.transactionRemoteStarted = false
761 connectorStatus.transactionStarted = false
762 connectorStatus.energyActiveImportRegisterValue = 0
763 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
764 if (connectorStatus.chargingProfiles == null) {
765 connectorStatus.chargingProfiles = []
766 }
767 }
768
769 const warnDeprecatedTemplateKey = (
770 template: ChargingStationTemplate,
771 key: string,
772 logPrefix: string,
773 templateFile: string,
774 logMsgToAppend = ''
775 ): void => {
776 if (template[key as keyof ChargingStationTemplate] != null) {
777 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
778 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
779 }`
780 logger.warn(`${logPrefix} ${logMsg}`)
781 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
782 }
783 }
784
785 const convertDeprecatedTemplateKey = (
786 template: ChargingStationTemplate,
787 deprecatedKey: string,
788 key?: string
789 ): void => {
790 if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
791 if (key != null) {
792 (template as unknown as Record<string, unknown>)[key] =
793 template[deprecatedKey as keyof ChargingStationTemplate]
794 }
795 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
796 delete template[deprecatedKey as keyof ChargingStationTemplate]
797 }
798 }
799
800 interface ChargingProfilesLimit {
801 limit: number
802 chargingProfile: ChargingProfile
803 }
804
805 /**
806 * Charging profiles shall already be sorted by connector id descending then stack level descending
807 *
808 * @param chargingStation -
809 * @param connectorId -
810 * @param chargingProfiles -
811 * @param logPrefix -
812 * @returns ChargingProfilesLimit
813 */
814 const getLimitFromChargingProfiles = (
815 chargingStation: ChargingStation,
816 connectorId: number,
817 chargingProfiles: ChargingProfile[],
818 logPrefix: string
819 ): ChargingProfilesLimit | undefined => {
820 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
821 const currentDate = new Date()
822 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
823 for (const chargingProfile of chargingProfiles) {
824 const chargingSchedule = chargingProfile.chargingSchedule
825 if (chargingSchedule.startSchedule == null) {
826 logger.debug(
827 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
828 )
829 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
830 chargingSchedule.startSchedule = connectorStatus?.transactionStart
831 }
832 if (!isDate(chargingSchedule.startSchedule)) {
833 logger.warn(
834 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
835 )
836 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
837 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
838 }
839 if (chargingSchedule.duration == null) {
840 logger.debug(
841 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
842 )
843 // OCPP specifies that if duration is not defined, it should be infinite
844 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
845 }
846 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
847 continue
848 }
849 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
850 continue
851 }
852 // Check if the charging profile is active
853 if (
854 isWithinInterval(currentDate, {
855 start: chargingSchedule.startSchedule,
856 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
857 })
858 ) {
859 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
860 const chargingSchedulePeriodCompareFn = (
861 a: ChargingSchedulePeriod,
862 b: ChargingSchedulePeriod
863 ): number => a.startPeriod - b.startPeriod
864 if (
865 !isArraySorted<ChargingSchedulePeriod>(
866 chargingSchedule.chargingSchedulePeriod,
867 chargingSchedulePeriodCompareFn
868 )
869 ) {
870 logger.warn(
871 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
872 )
873 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
874 }
875 // Check if the first schedule period startPeriod property is equal to 0
876 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
877 logger.error(
878 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
879 )
880 continue
881 }
882 // Handle only one schedule period
883 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
884 const result: ChargingProfilesLimit = {
885 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
886 chargingProfile
887 }
888 logger.debug(debugLogMsg, result)
889 return result
890 }
891 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
892 // Search for the right schedule period
893 for (const [
894 index,
895 chargingSchedulePeriod
896 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
897 // Find the right schedule period
898 if (
899 isAfter(
900 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
901 currentDate
902 )
903 ) {
904 // Found the schedule period: previous is the correct one
905 const result: ChargingProfilesLimit = {
906 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
907 limit: previousChargingSchedulePeriod!.limit,
908 chargingProfile
909 }
910 logger.debug(debugLogMsg, result)
911 return result
912 }
913 // Keep a reference to previous one
914 previousChargingSchedulePeriod = chargingSchedulePeriod
915 // Handle the last schedule period within the charging profile duration
916 if (
917 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
918 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
919 differenceInSeconds(
920 addSeconds(
921 chargingSchedule.startSchedule,
922 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
923 ),
924 chargingSchedule.startSchedule
925 ) > chargingSchedule.duration)
926 ) {
927 const result: ChargingProfilesLimit = {
928 limit: previousChargingSchedulePeriod.limit,
929 chargingProfile
930 }
931 logger.debug(debugLogMsg, result)
932 return result
933 }
934 }
935 }
936 }
937 }
938 }
939
940 export const prepareChargingProfileKind = (
941 connectorStatus: ConnectorStatus | undefined,
942 chargingProfile: ChargingProfile,
943 currentDate: string | number | Date,
944 logPrefix: string
945 ): boolean => {
946 switch (chargingProfile.chargingProfileKind) {
947 case ChargingProfileKindType.RECURRING:
948 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
949 return false
950 }
951 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
952 break
953 case ChargingProfileKindType.RELATIVE:
954 if (chargingProfile.chargingSchedule.startSchedule != null) {
955 logger.warn(
956 `${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`
957 )
958 delete chargingProfile.chargingSchedule.startSchedule
959 }
960 if (connectorStatus?.transactionStarted === true) {
961 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
962 }
963 // FIXME: Handle relative charging profile duration
964 break
965 }
966 return true
967 }
968
969 export const canProceedChargingProfile = (
970 chargingProfile: ChargingProfile,
971 currentDate: string | number | Date,
972 logPrefix: string
973 ): boolean => {
974 if (
975 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
976 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
977 ) {
978 logger.debug(
979 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
980 chargingProfile.chargingProfileId
981 } is not valid for the current date ${
982 isDate(currentDate) ? currentDate.toISOString() : currentDate
983 }`
984 )
985 return false
986 }
987 if (
988 chargingProfile.chargingSchedule.startSchedule == null ||
989 chargingProfile.chargingSchedule.duration == null
990 ) {
991 logger.error(
992 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
993 )
994 return false
995 }
996 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
997 logger.error(
998 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
999 )
1000 return false
1001 }
1002 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
1003 logger.error(
1004 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1005 )
1006 return false
1007 }
1008 return true
1009 }
1010
1011 const canProceedRecurringChargingProfile = (
1012 chargingProfile: ChargingProfile,
1013 logPrefix: string
1014 ): boolean => {
1015 if (
1016 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1017 chargingProfile.recurrencyKind == null
1018 ) {
1019 logger.error(
1020 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1021 )
1022 return false
1023 }
1024 if (
1025 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1026 chargingProfile.chargingSchedule.startSchedule == null
1027 ) {
1028 logger.error(
1029 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1030 )
1031 return false
1032 }
1033 return true
1034 }
1035
1036 /**
1037 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1038 *
1039 * @param chargingProfile -
1040 * @param currentDate -
1041 * @param logPrefix -
1042 */
1043 const prepareRecurringChargingProfile = (
1044 chargingProfile: ChargingProfile,
1045 currentDate: string | number | Date,
1046 logPrefix: string
1047 ): boolean => {
1048 const chargingSchedule = chargingProfile.chargingSchedule
1049 let recurringIntervalTranslated = false
1050 let recurringInterval: Interval | undefined
1051 switch (chargingProfile.recurrencyKind) {
1052 case RecurrencyKindType.DAILY:
1053 recurringInterval = {
1054 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1055 start: chargingSchedule.startSchedule!,
1056 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1057 end: addDays(chargingSchedule.startSchedule!, 1)
1058 }
1059 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1060 if (
1061 !isWithinInterval(currentDate, recurringInterval) &&
1062 isBefore(recurringInterval.end, currentDate)
1063 ) {
1064 chargingSchedule.startSchedule = addDays(
1065 recurringInterval.start,
1066 differenceInDays(currentDate, recurringInterval.start)
1067 )
1068 recurringInterval = {
1069 start: chargingSchedule.startSchedule,
1070 end: addDays(chargingSchedule.startSchedule, 1)
1071 }
1072 recurringIntervalTranslated = true
1073 }
1074 break
1075 case RecurrencyKindType.WEEKLY:
1076 recurringInterval = {
1077 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1078 start: chargingSchedule.startSchedule!,
1079 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1080 end: addWeeks(chargingSchedule.startSchedule!, 1)
1081 }
1082 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1083 if (
1084 !isWithinInterval(currentDate, recurringInterval) &&
1085 isBefore(recurringInterval.end, currentDate)
1086 ) {
1087 chargingSchedule.startSchedule = addWeeks(
1088 recurringInterval.start,
1089 differenceInWeeks(currentDate, recurringInterval.start)
1090 )
1091 recurringInterval = {
1092 start: chargingSchedule.startSchedule,
1093 end: addWeeks(chargingSchedule.startSchedule, 1)
1094 }
1095 recurringIntervalTranslated = true
1096 }
1097 break
1098 default:
1099 logger.error(
1100 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1101 )
1102 }
1103 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1104 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
1105 logger.error(
1106 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1107 chargingProfile.recurrencyKind
1108 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1109 recurringInterval?.start as Date
1110 ).toISOString()}, ${toDate(
1111 recurringInterval?.end as Date
1112 ).toISOString()}] has not been properly translated to current date ${
1113 isDate(currentDate) ? currentDate.toISOString() : currentDate
1114 } `
1115 )
1116 }
1117 return recurringIntervalTranslated
1118 }
1119
1120 const checkRecurringChargingProfileDuration = (
1121 chargingProfile: ChargingProfile,
1122 interval: Interval,
1123 logPrefix: string
1124 ): void => {
1125 if (chargingProfile.chargingSchedule.duration == null) {
1126 logger.warn(
1127 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1128 chargingProfile.chargingProfileKind
1129 } charging profile id ${
1130 chargingProfile.chargingProfileId
1131 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1132 interval.end,
1133 interval.start
1134 )}`
1135 )
1136 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1137 } else if (
1138 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
1139 ) {
1140 logger.warn(
1141 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1142 chargingProfile.chargingProfileKind
1143 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1144 chargingProfile.chargingSchedule.duration
1145 } is greater than the recurrency time interval duration ${differenceInSeconds(
1146 interval.end,
1147 interval.start
1148 )}`
1149 )
1150 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1151 }
1152 }
1153
1154 const getRandomSerialNumberSuffix = (params?: {
1155 randomBytesLength?: number
1156 upperCase?: boolean
1157 }): string => {
1158 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1159 if (params?.upperCase === true) {
1160 return randomSerialNumberSuffix.toUpperCase()
1161 }
1162 return randomSerialNumberSuffix
1163 }