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