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