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