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