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