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