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