refactor: remove isNullOrDefined() helper
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
1 import { createHash, randomBytes } from 'node:crypto'
2 import type { EventEmitter } from 'node:events'
3 import { basename, dirname, join } from 'node:path'
4 import { env } from 'node:process'
5 import { fileURLToPath } from 'node:url'
6
7 import chalk from 'chalk'
8 import {
9 type Interval,
10 addDays,
11 addSeconds,
12 addWeeks,
13 differenceInDays,
14 differenceInSeconds,
15 differenceInWeeks,
16 isAfter,
17 isBefore,
18 isDate,
19 isPast,
20 isWithinInterval,
21 toDate
22 } from 'date-fns'
23 import { maxTime } from 'date-fns/constants'
24
25 import type { ChargingStation } from './ChargingStation.js'
26 import { getConfigurationKey } from './ConfigurationKeyUtils.js'
27 import { BaseError } from '../exception/index.js'
28 import {
29 AmpereUnits,
30 AvailabilityType,
31 type BootNotificationRequest,
32 BootReasonEnumType,
33 type ChargingProfile,
34 ChargingProfileKindType,
35 ChargingRateUnitType,
36 type ChargingSchedulePeriod,
37 type ChargingStationConfiguration,
38 type ChargingStationInfo,
39 type ChargingStationTemplate,
40 type ChargingStationWorkerMessageEvents,
41 ConnectorPhaseRotation,
42 type ConnectorStatus,
43 ConnectorStatusEnum,
44 CurrentType,
45 type EvseTemplate,
46 type OCPP16BootNotificationRequest,
47 type OCPP20BootNotificationRequest,
48 OCPPVersion,
49 RecurrencyKindType,
50 type Reservation,
51 ReservationTerminationReason,
52 StandardParametersKey,
53 type SupportedFeatureProfiles,
54 Voltage
55 } from '../types/index.js'
56 import {
57 ACElectricUtils,
58 Constants,
59 DCElectricUtils,
60 cloneObject,
61 convertToDate,
62 convertToInt,
63 isArraySorted,
64 isEmptyObject,
65 isEmptyString,
66 isNotEmptyArray,
67 isNotEmptyString,
68 isUndefined,
69 isValidTime,
70 logger,
71 secureRandom
72 } from '../utils/index.js'
73
74 const moduleName = 'Helpers'
75
76 export const getChargingStationId = (
77 index: number,
78 stationTemplate: ChargingStationTemplate | undefined
79 ): string => {
80 if (stationTemplate == null) {
81 return "Unknown 'chargingStationId'"
82 }
83 // In case of multiple instances: add instance index to charging station id
84 const instanceIndex = env.CF_INSTANCE_INDEX ?? 0
85 const idSuffix = stationTemplate?.nameSuffix ?? ''
86 const idStr = `000000000${index.toString()}`
87 return stationTemplate?.fixedName === true
88 ? stationTemplate.baseName
89 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
90 idStr.length - 4
91 )}${idSuffix}`
92 }
93
94 export const hasReservationExpired = (reservation: Reservation): boolean => {
95 return isPast(reservation.expiryDate)
96 }
97
98 export const removeExpiredReservations = async (
99 chargingStation: ChargingStation
100 ): Promise<void> => {
101 if (chargingStation.hasEvses) {
102 for (const evseStatus of chargingStation.evses.values()) {
103 for (const connectorStatus of evseStatus.connectors.values()) {
104 if (
105 connectorStatus.reservation != null &&
106 hasReservationExpired(connectorStatus.reservation)
107 ) {
108 await chargingStation.removeReservation(
109 connectorStatus.reservation,
110 ReservationTerminationReason.EXPIRED
111 )
112 }
113 }
114 }
115 } else {
116 for (const connectorStatus of chargingStation.connectors.values()) {
117 if (
118 connectorStatus.reservation != null &&
119 hasReservationExpired(connectorStatus.reservation)
120 ) {
121 await chargingStation.removeReservation(
122 connectorStatus.reservation,
123 ReservationTerminationReason.EXPIRED
124 )
125 }
126 }
127 }
128 }
129
130 export const getNumberOfReservableConnectors = (
131 connectors: Map<number, ConnectorStatus>
132 ): number => {
133 let numberOfReservableConnectors = 0
134 for (const [connectorId, connectorStatus] of connectors) {
135 if (connectorId === 0) {
136 continue
137 }
138 if (connectorStatus.status === ConnectorStatusEnum.Available) {
139 ++numberOfReservableConnectors
140 }
141 }
142 return numberOfReservableConnectors
143 }
144
145 export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
146 const chargingStationInfo = {
147 chargePointModel: stationTemplate.chargePointModel,
148 chargePointVendor: stationTemplate.chargePointVendor,
149 ...(!isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
150 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix
151 }),
152 ...(!isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
153 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
154 }),
155 ...(!isUndefined(stationTemplate.meterSerialNumberPrefix) && {
156 meterSerialNumber: stationTemplate.meterSerialNumberPrefix
157 }),
158 ...(!isUndefined(stationTemplate.meterType) && {
159 meterType: stationTemplate.meterType
160 })
161 }
162 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
163 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
164 .digest('hex')
165 }
166
167 export const checkChargingStation = (
168 chargingStation: ChargingStation,
169 logPrefix: string
170 ): boolean => {
171 if (!chargingStation.started && !chargingStation.starting) {
172 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`)
173 return false
174 }
175 return true
176 }
177
178 export const getPhaseRotationValue = (
179 connectorId: number,
180 numberOfPhases: number
181 ): string | undefined => {
182 // AC/DC
183 if (connectorId === 0 && numberOfPhases === 0) {
184 return `${connectorId}.${ConnectorPhaseRotation.RST}`
185 } else if (connectorId > 0 && numberOfPhases === 0) {
186 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
187 // AC
188 } else if (connectorId >= 0 && numberOfPhases === 1) {
189 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
190 } else if (connectorId >= 0 && numberOfPhases === 3) {
191 return `${connectorId}.${ConnectorPhaseRotation.RST}`
192 }
193 }
194
195 export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
196 if (evses == null) {
197 return -1
198 }
199 return Object.keys(evses).length
200 }
201
202 const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
203 if (connectors == null) {
204 return -1
205 }
206 return Object.keys(connectors).length
207 }
208
209 export const getBootConnectorStatus = (
210 chargingStation: ChargingStation,
211 connectorId: number,
212 connectorStatus: ConnectorStatus
213 ): ConnectorStatusEnum => {
214 let connectorBootStatus: ConnectorStatusEnum
215 if (
216 connectorStatus?.status == null &&
217 (!chargingStation.isChargingStationAvailable() ||
218 !chargingStation.isConnectorAvailable(connectorId))
219 ) {
220 connectorBootStatus = ConnectorStatusEnum.Unavailable
221 } else if (connectorStatus?.status == null && connectorStatus?.bootStatus != null) {
222 // Set boot status in template at startup
223 connectorBootStatus = connectorStatus?.bootStatus
224 } else if (connectorStatus?.status != null) {
225 // Set previous status at startup
226 connectorBootStatus = connectorStatus?.status
227 } else {
228 // Set default status
229 connectorBootStatus = ConnectorStatusEnum.Available
230 }
231 return connectorBootStatus
232 }
233
234 export const checkTemplate = (
235 stationTemplate: ChargingStationTemplate,
236 logPrefix: string,
237 templateFile: string
238 ): void => {
239 if (stationTemplate == null) {
240 const errorMsg = `Failed to read charging station template file ${templateFile}`
241 logger.error(`${logPrefix} ${errorMsg}`)
242 throw new BaseError(errorMsg)
243 }
244 if (isEmptyObject(stationTemplate)) {
245 const errorMsg = `Empty charging station information from template file ${templateFile}`
246 logger.error(`${logPrefix} ${errorMsg}`)
247 throw new BaseError(errorMsg)
248 }
249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
251 stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION
252 logger.warn(
253 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
254 Constants.DEFAULT_ATG_CONFIGURATION
255 )
256 }
257 if (stationTemplate.idTagsFile == null || isEmptyString(stationTemplate.idTagsFile)) {
258 logger.warn(
259 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
260 )
261 }
262 }
263
264 export const checkConfiguration = (
265 stationConfiguration: ChargingStationConfiguration | undefined,
266 logPrefix: string,
267 configurationFile: string
268 ): void => {
269 if (stationConfiguration == null) {
270 const errorMsg = `Failed to read charging station configuration file ${configurationFile}`
271 logger.error(`${logPrefix} ${errorMsg}`)
272 throw new BaseError(errorMsg)
273 }
274 if (isEmptyObject(stationConfiguration)) {
275 const errorMsg = `Empty charging station configuration from file ${configurationFile}`
276 logger.error(`${logPrefix} ${errorMsg}`)
277 throw new BaseError(errorMsg)
278 }
279 }
280
281 export const checkConnectorsConfiguration = (
282 stationTemplate: ChargingStationTemplate,
283 logPrefix: string,
284 templateFile: string
285 ): {
286 configuredMaxConnectors: number
287 templateMaxConnectors: number
288 templateMaxAvailableConnectors: number
289 } => {
290 const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate)
291 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile)
292 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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 === false
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 (isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
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 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
402 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
403 }),
404 ...(!isUndefined(stationInfo.chargePointSerialNumber) && {
405 chargePointSerialNumber: stationInfo.chargePointSerialNumber
406 }),
407 ...(!isUndefined(stationInfo.firmwareVersion) && {
408 firmwareVersion: stationInfo.firmwareVersion
409 }),
410 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
411 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
412 ...(!isUndefined(stationInfo.meterSerialNumber) && {
413 meterSerialNumber: stationInfo.meterSerialNumber
414 }),
415 ...(!isUndefined(stationInfo.meterType) && {
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 ...(!isUndefined(stationInfo.firmwareVersion) && {
427 firmwareVersion: stationInfo.firmwareVersion
428 }),
429 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
430 serialNumber: stationInfo.chargeBoxSerialNumber
431 }),
432 ...((!isUndefined(stationInfo.iccid) || !isUndefined(stationInfo.imsi)) && {
433 modem: {
434 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
435 ...(!isUndefined(stationInfo.imsi) && { 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 !isUndefined(templateKey.key) ? `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,
507 stationInfoSrc: ChargingStationInfo,
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 ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
620 result
621 )
622 limit = connectorMaximumPower
623 }
624 }
625 }
626 return limit
627 }
628
629 export const getDefaultVoltageOut = (
630 currentType: CurrentType,
631 logPrefix: string,
632 templateFile: string
633 ): Voltage => {
634 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
635 let defaultVoltageOut: number
636 switch (currentType) {
637 case CurrentType.AC:
638 defaultVoltageOut = Voltage.VOLTAGE_230
639 break
640 case CurrentType.DC:
641 defaultVoltageOut = Voltage.VOLTAGE_400
642 break
643 default:
644 logger.error(`${logPrefix} ${errorMsg}`)
645 throw new BaseError(errorMsg)
646 }
647 return defaultVoltageOut
648 }
649
650 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
651 return stationInfo.idTagsFile != null
652 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
653 : undefined
654 }
655
656 export const waitChargingStationEvents = async (
657 emitter: EventEmitter,
658 event: ChargingStationWorkerMessageEvents,
659 eventsToWait: number
660 ): Promise<number> => {
661 return await new Promise<number>((resolve) => {
662 let events = 0
663 if (eventsToWait === 0) {
664 resolve(events)
665 return
666 }
667 emitter.on(event, () => {
668 ++events
669 if (events === eventsToWait) {
670 resolve(events)
671 }
672 })
673 })
674 }
675
676 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
677 let configuredMaxNumberOfConnectors = 0
678 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
679 const numberOfConnectors = stationTemplate.numberOfConnectors as number[]
680 configuredMaxNumberOfConnectors =
681 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
682 } else if (!isUndefined(stationTemplate.numberOfConnectors)) {
683 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number
684 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
685 configuredMaxNumberOfConnectors =
686 stationTemplate.Connectors?.[0] != null
687 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
688 : getMaxNumberOfConnectors(stationTemplate.Connectors)
689 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
690 for (const evse in stationTemplate.Evses) {
691 if (evse === '0') {
692 continue
693 }
694 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
695 stationTemplate.Evses[evse].Connectors
696 )
697 }
698 }
699 return configuredMaxNumberOfConnectors
700 }
701
702 const checkConfiguredMaxConnectors = (
703 configuredMaxConnectors: number,
704 logPrefix: string,
705 templateFile: string
706 ): void => {
707 if (configuredMaxConnectors <= 0) {
708 logger.warn(
709 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
710 )
711 }
712 }
713
714 const checkTemplateMaxConnectors = (
715 templateMaxConnectors: number,
716 logPrefix: string,
717 templateFile: string
718 ): void => {
719 if (templateMaxConnectors === 0) {
720 logger.warn(
721 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
722 )
723 } else if (templateMaxConnectors < 0) {
724 logger.error(
725 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
726 )
727 }
728 }
729
730 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
731 connectorStatus.availability = AvailabilityType.Operative
732 connectorStatus.idTagLocalAuthorized = false
733 connectorStatus.idTagAuthorized = false
734 connectorStatus.transactionRemoteStarted = false
735 connectorStatus.transactionStarted = false
736 connectorStatus.energyActiveImportRegisterValue = 0
737 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
738 if (isUndefined(connectorStatus.chargingProfiles)) {
739 connectorStatus.chargingProfiles = []
740 }
741 }
742
743 const warnDeprecatedTemplateKey = (
744 template: ChargingStationTemplate,
745 key: string,
746 logPrefix: string,
747 templateFile: string,
748 logMsgToAppend = ''
749 ): void => {
750 if (!isUndefined(template?.[key as keyof ChargingStationTemplate])) {
751 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
752 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
753 }`
754 logger.warn(`${logPrefix} ${logMsg}`)
755 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
756 }
757 }
758
759 const convertDeprecatedTemplateKey = (
760 template: ChargingStationTemplate,
761 deprecatedKey: string,
762 key?: string
763 ): void => {
764 if (!isUndefined(template?.[deprecatedKey as keyof ChargingStationTemplate])) {
765 if (!isUndefined(key)) {
766 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
767 (template as unknown as Record<string, unknown>)[key!] =
768 template[deprecatedKey as keyof ChargingStationTemplate]
769 }
770 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
771 delete template[deprecatedKey as keyof ChargingStationTemplate]
772 }
773 }
774
775 interface ChargingProfilesLimit {
776 limit: number
777 chargingProfile: ChargingProfile
778 }
779
780 /**
781 * Charging profiles shall already be sorted by connector id descending then stack level descending
782 *
783 * @param chargingStation -
784 * @param connectorId -
785 * @param chargingProfiles -
786 * @param logPrefix -
787 * @returns ChargingProfilesLimit
788 */
789 const getLimitFromChargingProfiles = (
790 chargingStation: ChargingStation,
791 connectorId: number,
792 chargingProfiles: ChargingProfile[],
793 logPrefix: string
794 ): ChargingProfilesLimit | undefined => {
795 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
796 const currentDate = new Date()
797 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
798 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
799 for (const chargingProfile of chargingProfiles) {
800 const chargingSchedule = chargingProfile.chargingSchedule
801 if (chargingSchedule?.startSchedule == null && connectorStatus?.transactionStarted === true) {
802 logger.debug(
803 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
804 )
805 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
806 chargingSchedule.startSchedule = connectorStatus?.transactionStart
807 }
808 if (chargingSchedule?.startSchedule != null && !isDate(chargingSchedule?.startSchedule)) {
809 logger.warn(
810 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
811 )
812 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
813 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!
814 }
815 if (chargingSchedule?.startSchedule != null && chargingSchedule?.duration == null) {
816 logger.debug(
817 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
818 )
819 // OCPP specifies that if duration is not defined, it should be infinite
820 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
821 }
822 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
823 continue
824 }
825 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
826 continue
827 }
828 // Check if the charging profile is active
829 if (
830 isWithinInterval(currentDate, {
831 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
832 start: chargingSchedule.startSchedule!,
833 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
834 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!)
835 })
836 ) {
837 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
838 const chargingSchedulePeriodCompareFn = (
839 a: ChargingSchedulePeriod,
840 b: ChargingSchedulePeriod
841 ): number => a.startPeriod - b.startPeriod
842 if (
843 !isArraySorted<ChargingSchedulePeriod>(
844 chargingSchedule.chargingSchedulePeriod,
845 chargingSchedulePeriodCompareFn
846 )
847 ) {
848 logger.warn(
849 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
850 )
851 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
852 }
853 // Check if the first schedule period startPeriod property is equal to 0
854 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
855 logger.error(
856 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
857 )
858 continue
859 }
860 // Handle only one schedule period
861 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
862 const result: ChargingProfilesLimit = {
863 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
864 chargingProfile
865 }
866 logger.debug(debugLogMsg, result)
867 return result
868 }
869 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
870 // Search for the right schedule period
871 for (const [
872 index,
873 chargingSchedulePeriod
874 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
875 // Find the right schedule period
876 if (
877 isAfter(
878 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
879 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
880 currentDate
881 )
882 ) {
883 // Found the schedule period: previous is the correct one
884 const result: ChargingProfilesLimit = {
885 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
886 limit: previousChargingSchedulePeriod!.limit,
887 chargingProfile
888 }
889 logger.debug(debugLogMsg, result)
890 return result
891 }
892 // Keep a reference to previous one
893 previousChargingSchedulePeriod = chargingSchedulePeriod
894 // Handle the last schedule period within the charging profile duration
895 if (
896 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
897 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
898 differenceInSeconds(
899 addSeconds(
900 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
901 chargingSchedule.startSchedule!,
902 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
903 ),
904 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
905 chargingSchedule.startSchedule!
906 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
907 ) > chargingSchedule.duration!)
908 ) {
909 const result: ChargingProfilesLimit = {
910 limit: previousChargingSchedulePeriod.limit,
911 chargingProfile
912 }
913 logger.debug(debugLogMsg, result)
914 return result
915 }
916 }
917 }
918 }
919 }
920 }
921
922 export const prepareChargingProfileKind = (
923 connectorStatus: ConnectorStatus,
924 chargingProfile: ChargingProfile,
925 currentDate: Date,
926 logPrefix: string
927 ): boolean => {
928 switch (chargingProfile.chargingProfileKind) {
929 case ChargingProfileKindType.RECURRING:
930 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
931 return false
932 }
933 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
934 break
935 case ChargingProfileKindType.RELATIVE:
936 if (chargingProfile.chargingSchedule.startSchedule != null) {
937 logger.warn(
938 `${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`
939 )
940 delete chargingProfile.chargingSchedule.startSchedule
941 }
942 if (connectorStatus?.transactionStarted === true) {
943 chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart
944 }
945 // FIXME: Handle relative charging profile duration
946 break
947 }
948 return true
949 }
950
951 export const canProceedChargingProfile = (
952 chargingProfile: ChargingProfile,
953 currentDate: Date,
954 logPrefix: string
955 ): boolean => {
956 if (
957 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
958 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
959 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
960 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
961 ) {
962 logger.debug(
963 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
964 chargingProfile.chargingProfileId
965 } is not valid for the current date ${currentDate.toISOString()}`
966 )
967 return false
968 }
969 if (
970 chargingProfile.chargingSchedule.startSchedule == null ||
971 chargingProfile.chargingSchedule.duration == null
972 ) {
973 logger.error(
974 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
975 )
976 return false
977 }
978 if (
979 chargingProfile.chargingSchedule.startSchedule != null &&
980 !isValidTime(chargingProfile.chargingSchedule.startSchedule)
981 ) {
982 logger.error(
983 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
984 )
985 return false
986 }
987 if (
988 chargingProfile.chargingSchedule.duration != null &&
989 !Number.isSafeInteger(chargingProfile.chargingSchedule.duration)
990 ) {
991 logger.error(
992 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
993 )
994 return false
995 }
996 return true
997 }
998
999 const canProceedRecurringChargingProfile = (
1000 chargingProfile: ChargingProfile,
1001 logPrefix: string
1002 ): boolean => {
1003 if (
1004 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1005 chargingProfile.recurrencyKind == null
1006 ) {
1007 logger.error(
1008 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1009 )
1010 return false
1011 }
1012 if (
1013 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1014 chargingProfile.chargingSchedule.startSchedule == null
1015 ) {
1016 logger.error(
1017 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1018 )
1019 return false
1020 }
1021 return true
1022 }
1023
1024 /**
1025 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1026 *
1027 * @param chargingProfile -
1028 * @param currentDate -
1029 * @param logPrefix -
1030 */
1031 const prepareRecurringChargingProfile = (
1032 chargingProfile: ChargingProfile,
1033 currentDate: Date,
1034 logPrefix: string
1035 ): boolean => {
1036 const chargingSchedule = chargingProfile.chargingSchedule
1037 let recurringIntervalTranslated = false
1038 let recurringInterval: Interval
1039 switch (chargingProfile.recurrencyKind) {
1040 case RecurrencyKindType.DAILY:
1041 recurringInterval = {
1042 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1043 start: chargingSchedule.startSchedule!,
1044 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1045 end: addDays(chargingSchedule.startSchedule!, 1)
1046 }
1047 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1048 if (
1049 !isWithinInterval(currentDate, recurringInterval) &&
1050 isBefore(recurringInterval.end, currentDate)
1051 ) {
1052 chargingSchedule.startSchedule = addDays(
1053 recurringInterval.start,
1054 differenceInDays(currentDate, recurringInterval.start)
1055 )
1056 recurringInterval = {
1057 start: chargingSchedule.startSchedule,
1058 end: addDays(chargingSchedule.startSchedule, 1)
1059 }
1060 recurringIntervalTranslated = true
1061 }
1062 break
1063 case RecurrencyKindType.WEEKLY:
1064 recurringInterval = {
1065 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1066 start: chargingSchedule.startSchedule!,
1067 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1068 end: addWeeks(chargingSchedule.startSchedule!, 1)
1069 }
1070 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1071 if (
1072 !isWithinInterval(currentDate, recurringInterval) &&
1073 isBefore(recurringInterval.end, currentDate)
1074 ) {
1075 chargingSchedule.startSchedule = addWeeks(
1076 recurringInterval.start,
1077 differenceInWeeks(currentDate, recurringInterval.start)
1078 )
1079 recurringInterval = {
1080 start: chargingSchedule.startSchedule,
1081 end: addWeeks(chargingSchedule.startSchedule, 1)
1082 }
1083 recurringIntervalTranslated = true
1084 }
1085 break
1086 default:
1087 logger.error(
1088 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1089 )
1090 }
1091 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1092 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
1093 logger.error(
1094 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1095 chargingProfile.recurrencyKind
1096 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1097 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1098 recurringInterval!.start
1099 ).toISOString()}, ${toDate(
1100 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1101 recurringInterval!.end
1102 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
1103 )
1104 }
1105 return recurringIntervalTranslated
1106 }
1107
1108 const checkRecurringChargingProfileDuration = (
1109 chargingProfile: ChargingProfile,
1110 interval: Interval,
1111 logPrefix: string
1112 ): void => {
1113 if (chargingProfile.chargingSchedule.duration == null) {
1114 logger.warn(
1115 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1116 chargingProfile.chargingProfileKind
1117 } charging profile id ${
1118 chargingProfile.chargingProfileId
1119 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1120 interval.end,
1121 interval.start
1122 )}`
1123 )
1124 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1125 } else if (
1126 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
1127 ) {
1128 logger.warn(
1129 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1130 chargingProfile.chargingProfileKind
1131 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1132 chargingProfile.chargingSchedule.duration
1133 } is greater than the recurrency time interval duration ${differenceInSeconds(
1134 interval.end,
1135 interval.start
1136 )}`
1137 )
1138 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1139 }
1140 }
1141
1142 const getRandomSerialNumberSuffix = (params?: {
1143 randomBytesLength?: number
1144 upperCase?: boolean
1145 }): string => {
1146 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1147 if (params?.upperCase === true) {
1148 return randomSerialNumberSuffix.toUpperCase()
1149 }
1150 return randomSerialNumberSuffix
1151 }