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