50b26885bbf999e21e5e92d243475a0a68c55871
[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 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 // 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 ${
352 connectors.get(connectorId)?.transactionId
353 }`
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 | undefined): void => {
371 if (connectorStatus == null) {
372 return
373 }
374 connectorStatus.chargingProfiles =
375 connectorStatus.transactionId != null && isNotEmptyArray(connectorStatus.chargingProfiles)
376 ? connectorStatus.chargingProfiles.filter(
377 chargingProfile => chargingProfile.transactionId !== connectorStatus.transactionId
378 )
379 : []
380 connectorStatus.idTagLocalAuthorized = false
381 connectorStatus.idTagAuthorized = false
382 connectorStatus.transactionRemoteStarted = false
383 connectorStatus.transactionStarted = false
384 delete connectorStatus.transactionStart
385 delete connectorStatus.transactionId
386 delete connectorStatus.localAuthorizeIdTag
387 delete connectorStatus.authorizeIdTag
388 delete connectorStatus.transactionIdTag
389 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
390 delete connectorStatus.transactionBeginMeterValue
391 }
392
393 export const createBootNotificationRequest = (
394 stationInfo: ChargingStationInfo,
395 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
396 ): BootNotificationRequest | undefined => {
397 const ocppVersion = stationInfo.ocppVersion
398 switch (ocppVersion) {
399 case OCPPVersion.VERSION_16:
400 return {
401 chargePointModel: stationInfo.chargePointModel,
402 chargePointVendor: stationInfo.chargePointVendor,
403 ...(stationInfo.chargeBoxSerialNumber !== undefined && {
404 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
405 }),
406 ...(stationInfo.chargePointSerialNumber !== undefined && {
407 chargePointSerialNumber: stationInfo.chargePointSerialNumber
408 }),
409 ...(stationInfo.firmwareVersion !== undefined && {
410 firmwareVersion: stationInfo.firmwareVersion
411 }),
412 ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
413 ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi }),
414 ...(stationInfo.meterSerialNumber !== undefined && {
415 meterSerialNumber: stationInfo.meterSerialNumber
416 }),
417 ...(stationInfo.meterType !== undefined && {
418 meterType: stationInfo.meterType
419 })
420 } satisfies OCPP16BootNotificationRequest
421 case OCPPVersion.VERSION_20:
422 case OCPPVersion.VERSION_201:
423 return {
424 reason: bootReason,
425 chargingStation: {
426 model: stationInfo.chargePointModel,
427 vendorName: stationInfo.chargePointVendor,
428 ...(stationInfo.firmwareVersion !== undefined && {
429 firmwareVersion: stationInfo.firmwareVersion
430 }),
431 ...(stationInfo.chargeBoxSerialNumber !== undefined && {
432 serialNumber: stationInfo.chargeBoxSerialNumber
433 }),
434 ...((stationInfo.iccid !== undefined || stationInfo.imsi !== undefined) && {
435 modem: {
436 ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
437 ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi })
438 }
439 })
440 }
441 } satisfies OCPP20BootNotificationRequest
442 }
443 }
444
445 export const warnTemplateKeysDeprecation = (
446 stationTemplate: ChargingStationTemplate,
447 logPrefix: string,
448 templateFile: string
449 ): void => {
450 const templateKeys: Array<{ deprecatedKey: string, key?: string }> = [
451 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
452 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
453 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
454 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' }
455 ]
456 for (const templateKey of templateKeys) {
457 warnDeprecatedTemplateKey(
458 stationTemplate,
459 templateKey.deprecatedKey,
460 logPrefix,
461 templateFile,
462 templateKey.key !== undefined ? `Use '${templateKey.key}' instead` : undefined
463 )
464 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
465 }
466 }
467
468 export const stationTemplateToStationInfo = (
469 stationTemplate: ChargingStationTemplate
470 ): ChargingStationInfo => {
471 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate)
472 delete stationTemplate.power
473 delete stationTemplate.powerUnit
474 delete stationTemplate.Connectors
475 delete stationTemplate.Evses
476 delete stationTemplate.Configuration
477 delete stationTemplate.AutomaticTransactionGenerator
478 delete stationTemplate.chargeBoxSerialNumberPrefix
479 delete stationTemplate.chargePointSerialNumberPrefix
480 delete stationTemplate.meterSerialNumberPrefix
481 return stationTemplate as ChargingStationInfo
482 }
483
484 export const createSerialNumber = (
485 stationTemplate: ChargingStationTemplate,
486 stationInfo: ChargingStationInfo,
487 params?: {
488 randomSerialNumberUpperCase?: boolean
489 randomSerialNumber?: boolean
490 }
491 ): void => {
492 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params }
493 const serialNumberSuffix =
494 params.randomSerialNumber === true
495 ? getRandomSerialNumberSuffix({
496 upperCase: params.randomSerialNumberUpperCase
497 })
498 : ''
499 isNotEmptyString(stationTemplate.chargePointSerialNumberPrefix) &&
500 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
501 isNotEmptyString(stationTemplate.chargeBoxSerialNumberPrefix) &&
502 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
503 isNotEmptyString(stationTemplate.meterSerialNumberPrefix) &&
504 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
505 }
506
507 export const propagateSerialNumber = (
508 stationTemplate: ChargingStationTemplate | undefined,
509 stationInfoSrc: ChargingStationInfo | undefined,
510 stationInfoDst: ChargingStationInfo
511 ): void => {
512 if (stationInfoSrc == null || stationTemplate == null) {
513 throw new BaseError(
514 'Missing charging station template or existing configuration to propagate serial number'
515 )
516 }
517 stationTemplate.chargePointSerialNumberPrefix != null &&
518 stationInfoSrc.chargePointSerialNumber != null
519 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
520 : stationInfoDst.chargePointSerialNumber != null &&
521 delete stationInfoDst.chargePointSerialNumber
522 stationTemplate.chargeBoxSerialNumberPrefix != null &&
523 stationInfoSrc.chargeBoxSerialNumber != null
524 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
525 : stationInfoDst.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
526 stationTemplate.meterSerialNumberPrefix != null && stationInfoSrc.meterSerialNumber != null
527 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
528 : stationInfoDst.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
529 }
530
531 export const hasFeatureProfile = (
532 chargingStation: ChargingStation,
533 featureProfile: SupportedFeatureProfiles
534 ): boolean | undefined => {
535 return getConfigurationKey(
536 chargingStation,
537 StandardParametersKey.SupportedFeatureProfiles
538 )?.value?.includes(featureProfile)
539 }
540
541 export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
542 let unitDivider = 1
543 switch (stationInfo.amperageLimitationUnit) {
544 case AmpereUnits.DECI_AMPERE:
545 unitDivider = 10
546 break
547 case AmpereUnits.CENTI_AMPERE:
548 unitDivider = 100
549 break
550 case AmpereUnits.MILLI_AMPERE:
551 unitDivider = 1000
552 break
553 }
554 return unitDivider
555 }
556
557 /**
558 * Gets the connector cloned charging profiles applying a power limitation
559 * and sorted by connector id descending then stack level descending
560 *
561 * @param chargingStation -
562 * @param connectorId -
563 * @returns connector charging profiles array
564 */
565 export const getConnectorChargingProfiles = (
566 chargingStation: ChargingStation,
567 connectorId: number
568 ): ChargingProfile[] => {
569 return cloneObject<ChargingProfile[]>(
570 (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
571 .sort((a, b) => b.stackLevel - a.stackLevel)
572 .concat(
573 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
574 (a, b) => b.stackLevel - a.stackLevel
575 )
576 )
577 )
578 }
579
580 export const getChargingStationConnectorChargingProfilesPowerLimit = (
581 chargingStation: ChargingStation,
582 connectorId: number
583 ): number | undefined => {
584 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
585 // Get charging profiles sorted by connector id then stack level
586 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
587 if (isNotEmptyArray(chargingProfiles)) {
588 const result = getLimitFromChargingProfiles(
589 chargingStation,
590 connectorId,
591 chargingProfiles,
592 chargingStation.logPrefix()
593 )
594 if (result != null) {
595 limit = result.limit
596 chargingProfile = result.chargingProfile
597 switch (chargingStation.stationInfo?.currentOutType) {
598 case CurrentType.AC:
599 limit =
600 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
601 ? limit
602 : ACElectricUtils.powerTotal(
603 chargingStation.getNumberOfPhases(),
604 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
605 chargingStation.stationInfo.voltageOut!,
606 limit
607 )
608 break
609 case CurrentType.DC:
610 limit =
611 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
612 ? limit
613 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
614 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
615 }
616 const connectorMaximumPower =
617 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
618 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
619 if (limit > connectorMaximumPower) {
620 logger.error(
621 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
622 chargingProfile.chargingProfileId
623 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
624 result
625 )
626 limit = connectorMaximumPower
627 }
628 }
629 }
630 return limit
631 }
632
633 export const getDefaultVoltageOut = (
634 currentType: CurrentType,
635 logPrefix: string,
636 templateFile: string
637 ): Voltage => {
638 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
639 let defaultVoltageOut: number
640 switch (currentType) {
641 case CurrentType.AC:
642 defaultVoltageOut = Voltage.VOLTAGE_230
643 break
644 case CurrentType.DC:
645 defaultVoltageOut = Voltage.VOLTAGE_400
646 break
647 default:
648 logger.error(`${logPrefix} ${errorMsg}`)
649 throw new BaseError(errorMsg)
650 }
651 return defaultVoltageOut
652 }
653
654 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
655 return stationInfo.idTagsFile != null
656 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
657 : undefined
658 }
659
660 export const waitChargingStationEvents = async (
661 emitter: EventEmitter,
662 event: ChargingStationWorkerMessageEvents,
663 eventsToWait: number
664 ): Promise<number> => {
665 return await new Promise<number>(resolve => {
666 let events = 0
667 if (eventsToWait === 0) {
668 resolve(events)
669 return
670 }
671 emitter.on(event, () => {
672 ++events
673 if (events === eventsToWait) {
674 resolve(events)
675 }
676 })
677 })
678 }
679
680 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
681 let configuredMaxNumberOfConnectors = 0
682 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
683 const numberOfConnectors = stationTemplate.numberOfConnectors
684 configuredMaxNumberOfConnectors =
685 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
686 } else if (stationTemplate.numberOfConnectors != null) {
687 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
688 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
689 configuredMaxNumberOfConnectors =
690 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
691 stationTemplate.Connectors[0] != null
692 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
693 : getMaxNumberOfConnectors(stationTemplate.Connectors)
694 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
695 for (const evse in stationTemplate.Evses) {
696 if (evse === '0') {
697 continue
698 }
699 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
700 stationTemplate.Evses[evse].Connectors
701 )
702 }
703 }
704 return configuredMaxNumberOfConnectors
705 }
706
707 const checkConfiguredMaxConnectors = (
708 configuredMaxConnectors: number,
709 logPrefix: string,
710 templateFile: string
711 ): void => {
712 if (configuredMaxConnectors <= 0) {
713 logger.warn(
714 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
715 )
716 }
717 }
718
719 const checkTemplateMaxConnectors = (
720 templateMaxConnectors: number,
721 logPrefix: string,
722 templateFile: string
723 ): void => {
724 if (templateMaxConnectors === 0) {
725 logger.warn(
726 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
727 )
728 } else if (templateMaxConnectors < 0) {
729 logger.error(
730 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
731 )
732 }
733 }
734
735 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
736 connectorStatus.availability = AvailabilityType.Operative
737 connectorStatus.idTagLocalAuthorized = false
738 connectorStatus.idTagAuthorized = false
739 connectorStatus.transactionRemoteStarted = false
740 connectorStatus.transactionStarted = false
741 connectorStatus.energyActiveImportRegisterValue = 0
742 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
743 if (connectorStatus.chargingProfiles == null) {
744 connectorStatus.chargingProfiles = []
745 }
746 }
747
748 const warnDeprecatedTemplateKey = (
749 template: ChargingStationTemplate,
750 key: string,
751 logPrefix: string,
752 templateFile: string,
753 logMsgToAppend = ''
754 ): void => {
755 if (template[key as keyof ChargingStationTemplate] !== undefined) {
756 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
757 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
758 }`
759 logger.warn(`${logPrefix} ${logMsg}`)
760 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
761 }
762 }
763
764 const convertDeprecatedTemplateKey = (
765 template: ChargingStationTemplate,
766 deprecatedKey: string,
767 key?: string
768 ): void => {
769 if (template[deprecatedKey as keyof ChargingStationTemplate] !== undefined) {
770 if (key !== undefined) {
771 (template as unknown as Record<string, unknown>)[key] =
772 template[deprecatedKey as keyof ChargingStationTemplate]
773 }
774 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
775 delete template[deprecatedKey as keyof ChargingStationTemplate]
776 }
777 }
778
779 interface ChargingProfilesLimit {
780 limit: number
781 chargingProfile: ChargingProfile
782 }
783
784 /**
785 * Charging profiles shall already be sorted by connector id descending then stack level descending
786 *
787 * @param chargingStation -
788 * @param connectorId -
789 * @param chargingProfiles -
790 * @param logPrefix -
791 * @returns ChargingProfilesLimit
792 */
793 const getLimitFromChargingProfiles = (
794 chargingStation: ChargingStation,
795 connectorId: number,
796 chargingProfiles: ChargingProfile[],
797 logPrefix: string
798 ): ChargingProfilesLimit | undefined => {
799 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
800 const currentDate = new Date()
801 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
802 for (const chargingProfile of chargingProfiles) {
803 const chargingSchedule = chargingProfile.chargingSchedule
804 if (chargingSchedule.startSchedule == null) {
805 logger.debug(
806 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
807 )
808 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
809 chargingSchedule.startSchedule = connectorStatus?.transactionStart
810 }
811 if (!isDate(chargingSchedule.startSchedule)) {
812 logger.warn(
813 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
814 )
815 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
816 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
817 }
818 if (chargingSchedule.duration == null) {
819 logger.debug(
820 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
821 )
822 // OCPP specifies that if duration is not defined, it should be infinite
823 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
824 }
825 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
826 continue
827 }
828 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
829 continue
830 }
831 // Check if the charging profile is active
832 if (
833 isWithinInterval(currentDate, {
834 start: chargingSchedule.startSchedule,
835 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
836 })
837 ) {
838 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
839 const chargingSchedulePeriodCompareFn = (
840 a: ChargingSchedulePeriod,
841 b: ChargingSchedulePeriod
842 ): number => a.startPeriod - b.startPeriod
843 if (
844 !isArraySorted<ChargingSchedulePeriod>(
845 chargingSchedule.chargingSchedulePeriod,
846 chargingSchedulePeriodCompareFn
847 )
848 ) {
849 logger.warn(
850 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
851 )
852 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
853 }
854 // Check if the first schedule period startPeriod property is equal to 0
855 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
856 logger.error(
857 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
858 )
859 continue
860 }
861 // Handle only one schedule period
862 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
863 const result: ChargingProfilesLimit = {
864 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
865 chargingProfile
866 }
867 logger.debug(debugLogMsg, result)
868 return result
869 }
870 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
871 // Search for the right schedule period
872 for (const [
873 index,
874 chargingSchedulePeriod
875 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
876 // Find the right schedule period
877 if (
878 isAfter(
879 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
880 currentDate
881 )
882 ) {
883 // Found the schedule period: previous is the correct one
884 const result: ChargingProfilesLimit = {
885 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
886 limit: previousChargingSchedulePeriod!.limit,
887 chargingProfile
888 }
889 logger.debug(debugLogMsg, result)
890 return result
891 }
892 // Keep a reference to previous one
893 previousChargingSchedulePeriod = chargingSchedulePeriod
894 // Handle the last schedule period within the charging profile duration
895 if (
896 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
897 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
898 differenceInSeconds(
899 addSeconds(
900 chargingSchedule.startSchedule,
901 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
902 ),
903 chargingSchedule.startSchedule
904 ) > chargingSchedule.duration)
905 ) {
906 const result: ChargingProfilesLimit = {
907 limit: previousChargingSchedulePeriod.limit,
908 chargingProfile
909 }
910 logger.debug(debugLogMsg, result)
911 return result
912 }
913 }
914 }
915 }
916 }
917 }
918
919 export const prepareChargingProfileKind = (
920 connectorStatus: ConnectorStatus | undefined,
921 chargingProfile: ChargingProfile,
922 currentDate: string | number | Date,
923 logPrefix: string
924 ): boolean => {
925 switch (chargingProfile.chargingProfileKind) {
926 case ChargingProfileKindType.RECURRING:
927 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
928 return false
929 }
930 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
931 break
932 case ChargingProfileKindType.RELATIVE:
933 if (chargingProfile.chargingSchedule.startSchedule != null) {
934 logger.warn(
935 `${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`
936 )
937 delete chargingProfile.chargingSchedule.startSchedule
938 }
939 if (connectorStatus?.transactionStarted === true) {
940 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
941 }
942 // FIXME: Handle relative charging profile duration
943 break
944 }
945 return true
946 }
947
948 export const canProceedChargingProfile = (
949 chargingProfile: ChargingProfile,
950 currentDate: string | number | Date,
951 logPrefix: string
952 ): boolean => {
953 if (
954 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
955 (isValidDate(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 isDate(currentDate) ? 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 (!isValidDate(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 | undefined
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 recurringInterval?.start as Date
1089 ).toISOString()}, ${toDate(
1090 recurringInterval?.end as Date
1091 ).toISOString()}] has not been properly translated to current date ${
1092 isDate(currentDate) ? currentDate.toISOString() : currentDate
1093 } `
1094 )
1095 }
1096 return recurringIntervalTranslated
1097 }
1098
1099 const checkRecurringChargingProfileDuration = (
1100 chargingProfile: ChargingProfile,
1101 interval: Interval,
1102 logPrefix: string
1103 ): void => {
1104 if (chargingProfile.chargingSchedule.duration == null) {
1105 logger.warn(
1106 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1107 chargingProfile.chargingProfileKind
1108 } charging profile id ${
1109 chargingProfile.chargingProfileId
1110 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1111 interval.end,
1112 interval.start
1113 )}`
1114 )
1115 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1116 } else if (
1117 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
1118 ) {
1119 logger.warn(
1120 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1121 chargingProfile.chargingProfileKind
1122 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1123 chargingProfile.chargingSchedule.duration
1124 } is greater than the recurrency time interval duration ${differenceInSeconds(
1125 interval.end,
1126 interval.start
1127 )}`
1128 )
1129 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1130 }
1131 }
1132
1133 const getRandomSerialNumberSuffix = (params?: {
1134 randomBytesLength?: number
1135 upperCase?: boolean
1136 }): string => {
1137 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1138 if (params?.upperCase === true) {
1139 return randomSerialNumberSuffix.toUpperCase()
1140 }
1141 return randomSerialNumberSuffix
1142 }