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