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