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