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