refactor: remove isUndefined() helper
[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>): number => {
195 if (evses == null) {
196 return -1
197 }
198 return Object.keys(evses).length
199 }
200
201 const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
202 if (connectors == null) {
203 return -1
204 }
205 return Object.keys(connectors).length
206 }
207
208 export const getBootConnectorStatus = (
209 chargingStation: ChargingStation,
210 connectorId: number,
211 connectorStatus: ConnectorStatus
212 ): ConnectorStatusEnum => {
213 let connectorBootStatus: ConnectorStatusEnum
214 if (
215 connectorStatus?.status == null &&
216 (!chargingStation.isChargingStationAvailable() ||
217 !chargingStation.isConnectorAvailable(connectorId))
218 ) {
219 connectorBootStatus = ConnectorStatusEnum.Unavailable
220 } else if (connectorStatus?.status == null && connectorStatus?.bootStatus != null) {
221 // Set boot status in template at startup
222 connectorBootStatus = connectorStatus?.bootStatus
223 } else if (connectorStatus?.status != null) {
224 // Set previous status at startup
225 connectorBootStatus = connectorStatus?.status
226 } else {
227 // Set default status
228 connectorBootStatus = ConnectorStatusEnum.Available
229 }
230 return connectorBootStatus
231 }
232
233 export const checkTemplate = (
234 stationTemplate: ChargingStationTemplate,
235 logPrefix: string,
236 templateFile: string
237 ): void => {
238 if (stationTemplate == null) {
239 const errorMsg = `Failed to read charging station template file ${templateFile}`
240 logger.error(`${logPrefix} ${errorMsg}`)
241 throw new BaseError(errorMsg)
242 }
243 if (isEmptyObject(stationTemplate)) {
244 const errorMsg = `Empty charging station information from template file ${templateFile}`
245 logger.error(`${logPrefix} ${errorMsg}`)
246 throw new BaseError(errorMsg)
247 }
248 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
249 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
250 stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION
251 logger.warn(
252 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
253 Constants.DEFAULT_ATG_CONFIGURATION
254 )
255 }
256 if (stationTemplate.idTagsFile == null || isEmptyString(stationTemplate.idTagsFile)) {
257 logger.warn(
258 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
259 )
260 }
261 }
262
263 export const checkConfiguration = (
264 stationConfiguration: ChargingStationConfiguration | undefined,
265 logPrefix: string,
266 configurationFile: string
267 ): void => {
268 if (stationConfiguration == null) {
269 const errorMsg = `Failed to read charging station configuration file ${configurationFile}`
270 logger.error(`${logPrefix} ${errorMsg}`)
271 throw new BaseError(errorMsg)
272 }
273 if (isEmptyObject(stationConfiguration)) {
274 const errorMsg = `Empty charging station configuration from file ${configurationFile}`
275 logger.error(`${logPrefix} ${errorMsg}`)
276 throw new BaseError(errorMsg)
277 }
278 }
279
280 export const checkConnectorsConfiguration = (
281 stationTemplate: ChargingStationTemplate,
282 logPrefix: string,
283 templateFile: string
284 ): {
285 configuredMaxConnectors: number
286 templateMaxConnectors: number
287 templateMaxAvailableConnectors: number
288 } => {
289 const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate)
290 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile)
291 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
292 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!)
293 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile)
294 const templateMaxAvailableConnectors =
295 stationTemplate.Connectors?.[0] != null ? templateMaxConnectors - 1 : templateMaxConnectors
296 if (
297 configuredMaxConnectors > templateMaxAvailableConnectors &&
298 stationTemplate?.randomConnectors === false
299 ) {
300 logger.warn(
301 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
302 )
303 stationTemplate.randomConnectors = true
304 }
305 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors }
306 }
307
308 export const checkStationInfoConnectorStatus = (
309 connectorId: number,
310 connectorStatus: ConnectorStatus,
311 logPrefix: string,
312 templateFile: string
313 ): void => {
314 if (connectorStatus?.status != null) {
315 logger.warn(
316 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
317 )
318 delete connectorStatus.status
319 }
320 }
321
322 export const buildConnectorsMap = (
323 connectors: Record<string, ConnectorStatus>,
324 logPrefix: string,
325 templateFile: string
326 ): Map<number, ConnectorStatus> => {
327 const connectorsMap = new Map<number, ConnectorStatus>()
328 if (getMaxNumberOfConnectors(connectors) > 0) {
329 for (const connector in connectors) {
330 const connectorStatus = connectors[connector]
331 const connectorId = convertToInt(connector)
332 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
333 connectorsMap.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus))
334 }
335 } else {
336 logger.warn(
337 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
338 )
339 }
340 return connectorsMap
341 }
342
343 export const initializeConnectorsMapStatus = (
344 connectors: Map<number, ConnectorStatus>,
345 logPrefix: string
346 ): void => {
347 for (const connectorId of connectors.keys()) {
348 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
349 logger.warn(
350 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
351 connectorId
352 )?.transactionId}`
353 )
354 }
355 if (connectorId === 0) {
356 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
357 connectors.get(connectorId)!.availability = AvailabilityType.Operative
358 if (connectors.get(connectorId)?.chargingProfiles == null) {
359 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
360 connectors.get(connectorId)!.chargingProfiles = []
361 }
362 } else if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted == null) {
363 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
364 initializeConnectorStatus(connectors.get(connectorId)!)
365 }
366 }
367 }
368
369 export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
370 connectorStatus.chargingProfiles =
371 connectorStatus.transactionId != null && isNotEmptyArray(connectorStatus.chargingProfiles)
372 ? connectorStatus.chargingProfiles?.filter(
373 (chargingProfile) => chargingProfile.transactionId !== connectorStatus.transactionId
374 )
375 : []
376 connectorStatus.idTagLocalAuthorized = false
377 connectorStatus.idTagAuthorized = false
378 connectorStatus.transactionRemoteStarted = false
379 connectorStatus.transactionStarted = false
380 delete connectorStatus?.transactionStart
381 delete connectorStatus?.transactionId
382 delete connectorStatus?.localAuthorizeIdTag
383 delete connectorStatus?.authorizeIdTag
384 delete connectorStatus?.transactionIdTag
385 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
386 delete connectorStatus?.transactionBeginMeterValue
387 }
388
389 export const createBootNotificationRequest = (
390 stationInfo: ChargingStationInfo,
391 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
392 ): BootNotificationRequest => {
393 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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,
506 stationInfoSrc: ChargingStationInfo,
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 ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
619 result
620 )
621 limit = connectorMaximumPower
622 }
623 }
624 }
625 return limit
626 }
627
628 export const getDefaultVoltageOut = (
629 currentType: CurrentType,
630 logPrefix: string,
631 templateFile: string
632 ): Voltage => {
633 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
634 let defaultVoltageOut: number
635 switch (currentType) {
636 case CurrentType.AC:
637 defaultVoltageOut = Voltage.VOLTAGE_230
638 break
639 case CurrentType.DC:
640 defaultVoltageOut = Voltage.VOLTAGE_400
641 break
642 default:
643 logger.error(`${logPrefix} ${errorMsg}`)
644 throw new BaseError(errorMsg)
645 }
646 return defaultVoltageOut
647 }
648
649 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
650 return stationInfo.idTagsFile != null
651 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
652 : undefined
653 }
654
655 export const waitChargingStationEvents = async (
656 emitter: EventEmitter,
657 event: ChargingStationWorkerMessageEvents,
658 eventsToWait: number
659 ): Promise<number> => {
660 return await new Promise<number>((resolve) => {
661 let events = 0
662 if (eventsToWait === 0) {
663 resolve(events)
664 return
665 }
666 emitter.on(event, () => {
667 ++events
668 if (events === eventsToWait) {
669 resolve(events)
670 }
671 })
672 })
673 }
674
675 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
676 let configuredMaxNumberOfConnectors = 0
677 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
678 const numberOfConnectors = stationTemplate.numberOfConnectors as number[]
679 configuredMaxNumberOfConnectors =
680 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
681 } else if (stationTemplate.numberOfConnectors != null) {
682 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number
683 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
684 configuredMaxNumberOfConnectors =
685 stationTemplate.Connectors?.[0] != null
686 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
687 : getMaxNumberOfConnectors(stationTemplate.Connectors)
688 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
689 for (const evse in stationTemplate.Evses) {
690 if (evse === '0') {
691 continue
692 }
693 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
694 stationTemplate.Evses[evse].Connectors
695 )
696 }
697 }
698 return configuredMaxNumberOfConnectors
699 }
700
701 const checkConfiguredMaxConnectors = (
702 configuredMaxConnectors: number,
703 logPrefix: string,
704 templateFile: string
705 ): void => {
706 if (configuredMaxConnectors <= 0) {
707 logger.warn(
708 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
709 )
710 }
711 }
712
713 const checkTemplateMaxConnectors = (
714 templateMaxConnectors: number,
715 logPrefix: string,
716 templateFile: string
717 ): void => {
718 if (templateMaxConnectors === 0) {
719 logger.warn(
720 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
721 )
722 } else if (templateMaxConnectors < 0) {
723 logger.error(
724 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
725 )
726 }
727 }
728
729 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
730 connectorStatus.availability = AvailabilityType.Operative
731 connectorStatus.idTagLocalAuthorized = false
732 connectorStatus.idTagAuthorized = false
733 connectorStatus.transactionRemoteStarted = false
734 connectorStatus.transactionStarted = false
735 connectorStatus.energyActiveImportRegisterValue = 0
736 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
737 if (connectorStatus.chargingProfiles == null) {
738 connectorStatus.chargingProfiles = []
739 }
740 }
741
742 const warnDeprecatedTemplateKey = (
743 template: ChargingStationTemplate,
744 key: string,
745 logPrefix: string,
746 templateFile: string,
747 logMsgToAppend = ''
748 ): void => {
749 if (template?.[key as keyof ChargingStationTemplate] !== undefined) {
750 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
751 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
752 }`
753 logger.warn(`${logPrefix} ${logMsg}`)
754 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
755 }
756 }
757
758 const convertDeprecatedTemplateKey = (
759 template: ChargingStationTemplate,
760 deprecatedKey: string,
761 key?: string
762 ): void => {
763 if (template?.[deprecatedKey as keyof ChargingStationTemplate] !== undefined) {
764 if (key !== undefined) {
765 (template as unknown as Record<string, unknown>)[key] =
766 template[deprecatedKey as keyof ChargingStationTemplate]
767 }
768 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
769 delete template[deprecatedKey as keyof ChargingStationTemplate]
770 }
771 }
772
773 interface ChargingProfilesLimit {
774 limit: number
775 chargingProfile: ChargingProfile
776 }
777
778 /**
779 * Charging profiles shall already be sorted by connector id descending then stack level descending
780 *
781 * @param chargingStation -
782 * @param connectorId -
783 * @param chargingProfiles -
784 * @param logPrefix -
785 * @returns ChargingProfilesLimit
786 */
787 const getLimitFromChargingProfiles = (
788 chargingStation: ChargingStation,
789 connectorId: number,
790 chargingProfiles: ChargingProfile[],
791 logPrefix: string
792 ): ChargingProfilesLimit | undefined => {
793 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
794 const currentDate = new Date()
795 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
796 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
797 for (const chargingProfile of chargingProfiles) {
798 const chargingSchedule = chargingProfile.chargingSchedule
799 if (chargingSchedule?.startSchedule == null && connectorStatus?.transactionStarted === true) {
800 logger.debug(
801 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
802 )
803 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
804 chargingSchedule.startSchedule = connectorStatus?.transactionStart
805 }
806 if (chargingSchedule?.startSchedule != null && !isDate(chargingSchedule?.startSchedule)) {
807 logger.warn(
808 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
809 )
810 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
811 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!
812 }
813 if (chargingSchedule?.startSchedule != null && chargingSchedule?.duration == null) {
814 logger.debug(
815 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
816 )
817 // OCPP specifies that if duration is not defined, it should be infinite
818 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
819 }
820 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
821 continue
822 }
823 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
824 continue
825 }
826 // Check if the charging profile is active
827 if (
828 isWithinInterval(currentDate, {
829 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
830 start: chargingSchedule.startSchedule!,
831 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
832 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!)
833 })
834 ) {
835 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
836 const chargingSchedulePeriodCompareFn = (
837 a: ChargingSchedulePeriod,
838 b: ChargingSchedulePeriod
839 ): number => a.startPeriod - b.startPeriod
840 if (
841 !isArraySorted<ChargingSchedulePeriod>(
842 chargingSchedule.chargingSchedulePeriod,
843 chargingSchedulePeriodCompareFn
844 )
845 ) {
846 logger.warn(
847 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
848 )
849 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
850 }
851 // Check if the first schedule period startPeriod property is equal to 0
852 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
853 logger.error(
854 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
855 )
856 continue
857 }
858 // Handle only one schedule period
859 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
860 const result: ChargingProfilesLimit = {
861 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
862 chargingProfile
863 }
864 logger.debug(debugLogMsg, result)
865 return result
866 }
867 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
868 // Search for the right schedule period
869 for (const [
870 index,
871 chargingSchedulePeriod
872 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
873 // Find the right schedule period
874 if (
875 isAfter(
876 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
899 chargingSchedule.startSchedule!,
900 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
901 ),
902 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
903 chargingSchedule.startSchedule!
904 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
905 ) > chargingSchedule.duration!)
906 ) {
907 const result: ChargingProfilesLimit = {
908 limit: previousChargingSchedulePeriod.limit,
909 chargingProfile
910 }
911 logger.debug(debugLogMsg, result)
912 return result
913 }
914 }
915 }
916 }
917 }
918 }
919
920 export const prepareChargingProfileKind = (
921 connectorStatus: ConnectorStatus,
922 chargingProfile: ChargingProfile,
923 currentDate: Date,
924 logPrefix: string
925 ): boolean => {
926 switch (chargingProfile.chargingProfileKind) {
927 case ChargingProfileKindType.RECURRING:
928 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
929 return false
930 }
931 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
932 break
933 case ChargingProfileKindType.RELATIVE:
934 if (chargingProfile.chargingSchedule.startSchedule != null) {
935 logger.warn(
936 `${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`
937 )
938 delete chargingProfile.chargingSchedule.startSchedule
939 }
940 if (connectorStatus?.transactionStarted === true) {
941 chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart
942 }
943 // FIXME: Handle relative charging profile duration
944 break
945 }
946 return true
947 }
948
949 export const canProceedChargingProfile = (
950 chargingProfile: ChargingProfile,
951 currentDate: Date,
952 logPrefix: string
953 ): boolean => {
954 if (
955 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
956 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
957 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
958 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
959 ) {
960 logger.debug(
961 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
962 chargingProfile.chargingProfileId
963 } is not valid for the current date ${currentDate.toISOString()}`
964 )
965 return false
966 }
967 if (
968 chargingProfile.chargingSchedule.startSchedule == null ||
969 chargingProfile.chargingSchedule.duration == null
970 ) {
971 logger.error(
972 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
973 )
974 return false
975 }
976 if (
977 chargingProfile.chargingSchedule.startSchedule != null &&
978 !isValidTime(chargingProfile.chargingSchedule.startSchedule)
979 ) {
980 logger.error(
981 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
982 )
983 return false
984 }
985 if (
986 chargingProfile.chargingSchedule.duration != null &&
987 !Number.isSafeInteger(chargingProfile.chargingSchedule.duration)
988 ) {
989 logger.error(
990 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
991 )
992 return false
993 }
994 return true
995 }
996
997 const canProceedRecurringChargingProfile = (
998 chargingProfile: ChargingProfile,
999 logPrefix: string
1000 ): boolean => {
1001 if (
1002 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1003 chargingProfile.recurrencyKind == null
1004 ) {
1005 logger.error(
1006 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1007 )
1008 return false
1009 }
1010 if (
1011 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1012 chargingProfile.chargingSchedule.startSchedule == null
1013 ) {
1014 logger.error(
1015 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1016 )
1017 return false
1018 }
1019 return true
1020 }
1021
1022 /**
1023 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1024 *
1025 * @param chargingProfile -
1026 * @param currentDate -
1027 * @param logPrefix -
1028 */
1029 const prepareRecurringChargingProfile = (
1030 chargingProfile: ChargingProfile,
1031 currentDate: Date,
1032 logPrefix: string
1033 ): boolean => {
1034 const chargingSchedule = chargingProfile.chargingSchedule
1035 let recurringIntervalTranslated = false
1036 let recurringInterval: Interval
1037 switch (chargingProfile.recurrencyKind) {
1038 case RecurrencyKindType.DAILY:
1039 recurringInterval = {
1040 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1041 start: chargingSchedule.startSchedule!,
1042 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1043 end: addDays(chargingSchedule.startSchedule!, 1)
1044 }
1045 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1046 if (
1047 !isWithinInterval(currentDate, recurringInterval) &&
1048 isBefore(recurringInterval.end, currentDate)
1049 ) {
1050 chargingSchedule.startSchedule = addDays(
1051 recurringInterval.start,
1052 differenceInDays(currentDate, recurringInterval.start)
1053 )
1054 recurringInterval = {
1055 start: chargingSchedule.startSchedule,
1056 end: addDays(chargingSchedule.startSchedule, 1)
1057 }
1058 recurringIntervalTranslated = true
1059 }
1060 break
1061 case RecurrencyKindType.WEEKLY:
1062 recurringInterval = {
1063 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1064 start: chargingSchedule.startSchedule!,
1065 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1066 end: addWeeks(chargingSchedule.startSchedule!, 1)
1067 }
1068 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1069 if (
1070 !isWithinInterval(currentDate, recurringInterval) &&
1071 isBefore(recurringInterval.end, currentDate)
1072 ) {
1073 chargingSchedule.startSchedule = addWeeks(
1074 recurringInterval.start,
1075 differenceInWeeks(currentDate, recurringInterval.start)
1076 )
1077 recurringInterval = {
1078 start: chargingSchedule.startSchedule,
1079 end: addWeeks(chargingSchedule.startSchedule, 1)
1080 }
1081 recurringIntervalTranslated = true
1082 }
1083 break
1084 default:
1085 logger.error(
1086 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1087 )
1088 }
1089 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1090 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
1091 logger.error(
1092 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1093 chargingProfile.recurrencyKind
1094 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1095 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1096 recurringInterval!.start
1097 ).toISOString()}, ${toDate(
1098 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1099 recurringInterval!.end
1100 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
1101 )
1102 }
1103 return recurringIntervalTranslated
1104 }
1105
1106 const checkRecurringChargingProfileDuration = (
1107 chargingProfile: ChargingProfile,
1108 interval: Interval,
1109 logPrefix: string
1110 ): void => {
1111 if (chargingProfile.chargingSchedule.duration == null) {
1112 logger.warn(
1113 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1114 chargingProfile.chargingProfileKind
1115 } charging profile id ${
1116 chargingProfile.chargingProfileId
1117 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1118 interval.end,
1119 interval.start
1120 )}`
1121 )
1122 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1123 } else if (
1124 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
1125 ) {
1126 logger.warn(
1127 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1128 chargingProfile.chargingProfileKind
1129 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1130 chargingProfile.chargingSchedule.duration
1131 } is greater than the recurrency time interval duration ${differenceInSeconds(
1132 interval.end,
1133 interval.start
1134 )}`
1135 )
1136 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1137 }
1138 }
1139
1140 const getRandomSerialNumberSuffix = (params?: {
1141 randomBytesLength?: number
1142 upperCase?: boolean
1143 }): string => {
1144 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1145 if (params?.upperCase === true) {
1146 return randomSerialNumberSuffix.toUpperCase()
1147 }
1148 return randomSerialNumberSuffix
1149 }