fix: avoid to modify stored charging profiles
[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 ChargingProfilePurposeType,
35 ChargingRateUnitType,
36 type ChargingSchedulePeriod,
37 type ChargingStationConfiguration,
38 type ChargingStationInfo,
39 type ChargingStationOptions,
40 type ChargingStationTemplate,
41 type ChargingStationWorkerMessageEvents,
42 ConnectorPhaseRotation,
43 type ConnectorStatus,
44 ConnectorStatusEnum,
45 CurrentType,
46 type EvseTemplate,
47 type OCPP16BootNotificationRequest,
48 type OCPP20BootNotificationRequest,
49 OCPPVersion,
50 RecurrencyKindType,
51 type Reservation,
52 ReservationTerminationReason,
53 StandardParametersKey,
54 type SupportedFeatureProfiles,
55 Voltage
56 } from '../types/index.js'
57 import {
58 ACElectricUtils,
59 clone,
60 Constants,
61 convertToDate,
62 convertToInt,
63 DCElectricUtils,
64 isArraySorted,
65 isNotEmptyArray,
66 isNotEmptyString,
67 isValidDate,
68 logger,
69 secureRandom
70 } from '../utils/index.js'
71 import type { ChargingStation } from './ChargingStation.js'
72 import { getConfigurationKey } from './ConfigurationKeyUtils.js'
73
74 const moduleName = 'Helpers'
75
76 export const buildTemplateName = (templateFile: string): string => {
77 if (isAbsolute(templateFile)) {
78 templateFile = relative(
79 resolve(join(dirname(fileURLToPath(import.meta.url)), 'assets', 'station-templates')),
80 templateFile
81 )
82 }
83 const templateFileParsedPath = parse(templateFile)
84 return join(templateFileParsedPath.dir, templateFileParsedPath.name)
85 }
86
87 export const getChargingStationId = (
88 index: number,
89 stationTemplate: ChargingStationTemplate | undefined
90 ): string => {
91 if (stationTemplate == null) {
92 return "Unknown 'chargingStationId'"
93 }
94 // In case of multiple instances: add instance index to charging station id
95 const instanceIndex = env.CF_INSTANCE_INDEX ?? 0
96 const idSuffix = stationTemplate.nameSuffix ?? ''
97 const idStr = `000000000${index.toString()}`
98 return stationTemplate.fixedName === true
99 ? stationTemplate.baseName
100 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
101 idStr.length - 4
102 )}${idSuffix}`
103 }
104
105 export const hasReservationExpired = (reservation: Reservation): boolean => {
106 return isPast(reservation.expiryDate)
107 }
108
109 export const removeExpiredReservations = async (
110 chargingStation: ChargingStation
111 ): Promise<void> => {
112 if (chargingStation.hasEvses) {
113 for (const evseStatus of chargingStation.evses.values()) {
114 for (const connectorStatus of evseStatus.connectors.values()) {
115 if (
116 connectorStatus.reservation != null &&
117 hasReservationExpired(connectorStatus.reservation)
118 ) {
119 await chargingStation.removeReservation(
120 connectorStatus.reservation,
121 ReservationTerminationReason.EXPIRED
122 )
123 }
124 }
125 }
126 } else {
127 for (const connectorStatus of chargingStation.connectors.values()) {
128 if (
129 connectorStatus.reservation != null &&
130 hasReservationExpired(connectorStatus.reservation)
131 ) {
132 await chargingStation.removeReservation(
133 connectorStatus.reservation,
134 ReservationTerminationReason.EXPIRED
135 )
136 }
137 }
138 }
139 }
140
141 export const getNumberOfReservableConnectors = (
142 connectors: Map<number, ConnectorStatus>
143 ): number => {
144 let numberOfReservableConnectors = 0
145 for (const [connectorId, connectorStatus] of connectors) {
146 if (connectorId === 0) {
147 continue
148 }
149 if (connectorStatus.status === ConnectorStatusEnum.Available) {
150 ++numberOfReservableConnectors
151 }
152 }
153 return numberOfReservableConnectors
154 }
155
156 export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
157 const chargingStationInfo = {
158 chargePointModel: stationTemplate.chargePointModel,
159 chargePointVendor: stationTemplate.chargePointVendor,
160 ...(stationTemplate.chargeBoxSerialNumberPrefix != null && {
161 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix
162 }),
163 ...(stationTemplate.chargePointSerialNumberPrefix != null && {
164 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
165 }),
166 ...(stationTemplate.meterSerialNumberPrefix != null && {
167 meterSerialNumber: stationTemplate.meterSerialNumberPrefix
168 }),
169 ...(stationTemplate.meterType != null && {
170 meterType: stationTemplate.meterType
171 })
172 }
173 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
174 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
175 .digest('hex')
176 }
177
178 export const checkChargingStation = (
179 chargingStation: ChargingStation,
180 logPrefix: string
181 ): boolean => {
182 if (!chargingStation.started && !chargingStation.starting) {
183 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`)
184 return false
185 }
186 return true
187 }
188
189 export const getPhaseRotationValue = (
190 connectorId: number,
191 numberOfPhases: number
192 ): string | undefined => {
193 // AC/DC
194 if (connectorId === 0 && numberOfPhases === 0) {
195 return `${connectorId}.${ConnectorPhaseRotation.RST}`
196 } else if (connectorId > 0 && numberOfPhases === 0) {
197 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
198 // AC
199 } else if (connectorId >= 0 && numberOfPhases === 1) {
200 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`
201 } else if (connectorId >= 0 && numberOfPhases === 3) {
202 return `${connectorId}.${ConnectorPhaseRotation.RST}`
203 }
204 }
205
206 export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate> | undefined): number => {
207 if (evses == null) {
208 return -1
209 }
210 return Object.keys(evses).length
211 }
212
213 const getMaxNumberOfConnectors = (
214 connectors: Record<string, ConnectorStatus> | undefined
215 ): number => {
216 if (connectors == null) {
217 return -1
218 }
219 return Object.keys(connectors).length
220 }
221
222 export const getBootConnectorStatus = (
223 chargingStation: ChargingStation,
224 connectorId: number,
225 connectorStatus: ConnectorStatus
226 ): ConnectorStatusEnum => {
227 let connectorBootStatus: ConnectorStatusEnum
228 if (
229 connectorStatus.status == null &&
230 (!chargingStation.isChargingStationAvailable() ||
231 !chargingStation.isConnectorAvailable(connectorId))
232 ) {
233 connectorBootStatus = ConnectorStatusEnum.Unavailable
234 } else if (connectorStatus.status == null && connectorStatus.bootStatus != null) {
235 // Set boot status in template at startup
236 connectorBootStatus = connectorStatus.bootStatus
237 } else if (connectorStatus.status != null) {
238 // Set previous status at startup
239 connectorBootStatus = connectorStatus.status
240 } else {
241 // Set default status
242 connectorBootStatus = ConnectorStatusEnum.Available
243 }
244 return connectorBootStatus
245 }
246
247 export const checkTemplate = (
248 stationTemplate: ChargingStationTemplate | undefined,
249 logPrefix: string,
250 templateFile: string
251 ): void => {
252 if (stationTemplate == null) {
253 const errorMsg = `Failed to read charging station template file ${templateFile}`
254 logger.error(`${logPrefix} ${errorMsg}`)
255 throw new BaseError(errorMsg)
256 }
257 if (isEmpty(stationTemplate)) {
258 const errorMsg = `Empty charging station information from template file ${templateFile}`
259 logger.error(`${logPrefix} ${errorMsg}`)
260 throw new BaseError(errorMsg)
261 }
262 if (stationTemplate.idTagsFile == null || isEmpty(stationTemplate.idTagsFile)) {
263 logger.warn(
264 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
265 )
266 }
267 }
268
269 export const checkConfiguration = (
270 stationConfiguration: ChargingStationConfiguration | undefined,
271 logPrefix: string,
272 configurationFile: string
273 ): void => {
274 if (stationConfiguration == null) {
275 const errorMsg = `Failed to read charging station configuration file ${configurationFile}`
276 logger.error(`${logPrefix} ${errorMsg}`)
277 throw new BaseError(errorMsg)
278 }
279 if (isEmpty(stationConfiguration)) {
280 const errorMsg = `Empty charging station configuration from file ${configurationFile}`
281 logger.error(`${logPrefix} ${errorMsg}`)
282 throw new BaseError(errorMsg)
283 }
284 }
285
286 export const checkConnectorsConfiguration = (
287 stationTemplate: ChargingStationTemplate,
288 logPrefix: string,
289 templateFile: string
290 ): {
291 configuredMaxConnectors: number
292 templateMaxConnectors: number
293 templateMaxAvailableConnectors: number
294 } => {
295 const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate)
296 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile)
297 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors)
298 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile)
299 const templateMaxAvailableConnectors =
300 stationTemplate.Connectors?.[0] != null ? templateMaxConnectors - 1 : templateMaxConnectors
301 if (
302 configuredMaxConnectors > templateMaxAvailableConnectors &&
303 stationTemplate.randomConnectors !== true
304 ) {
305 logger.warn(
306 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
307 )
308 stationTemplate.randomConnectors = true
309 }
310 return {
311 configuredMaxConnectors,
312 templateMaxConnectors,
313 templateMaxAvailableConnectors
314 }
315 }
316
317 export const checkStationInfoConnectorStatus = (
318 connectorId: number,
319 connectorStatus: ConnectorStatus,
320 logPrefix: string,
321 templateFile: string
322 ): void => {
323 if (connectorStatus.status != null) {
324 logger.warn(
325 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`
326 )
327 delete connectorStatus.status
328 }
329 }
330
331 export const setChargingStationOptions = (
332 stationInfo: ChargingStationInfo,
333 options?: ChargingStationOptions
334 ): ChargingStationInfo => {
335 if (options?.supervisionUrls != null) {
336 stationInfo.supervisionUrls = options.supervisionUrls
337 }
338 if (options?.persistentConfiguration != null) {
339 stationInfo.stationInfoPersistentConfiguration = options.persistentConfiguration
340 stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
341 stationInfo.automaticTransactionGeneratorPersistentConfiguration =
342 options.persistentConfiguration
343 }
344 if (options?.autoStart != null) {
345 stationInfo.autoStart = options.autoStart
346 }
347 if (options?.autoRegister != null) {
348 stationInfo.autoRegister = options.autoRegister
349 }
350 if (options?.enableStatistics != null) {
351 stationInfo.enableStatistics = options.enableStatistics
352 }
353 if (options?.ocppStrictCompliance != null) {
354 stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
355 }
356 if (options?.stopTransactionsOnStopped != null) {
357 stationInfo.stopTransactionsOnStopped = options.stopTransactionsOnStopped
358 }
359 return stationInfo
360 }
361
362 export const buildConnectorsMap = (
363 connectors: Record<string, ConnectorStatus>,
364 logPrefix: string,
365 templateFile: string
366 ): Map<number, ConnectorStatus> => {
367 const connectorsMap = new Map<number, ConnectorStatus>()
368 if (getMaxNumberOfConnectors(connectors) > 0) {
369 for (const connector in connectors) {
370 const connectorStatus = connectors[connector]
371 const connectorId = convertToInt(connector)
372 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
373 connectorsMap.set(connectorId, clone<ConnectorStatus>(connectorStatus))
374 }
375 } else {
376 logger.warn(
377 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
378 )
379 }
380 return connectorsMap
381 }
382
383 export const initializeConnectorsMapStatus = (
384 connectors: Map<number, ConnectorStatus>,
385 logPrefix: string
386 ): void => {
387 for (const connectorId of connectors.keys()) {
388 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
389 logger.warn(
390 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
391 connectors.get(connectorId)?.transactionId
392 }`
393 )
394 }
395 if (connectorId === 0) {
396 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
397 connectors.get(connectorId)!.availability = AvailabilityType.Operative
398 if (connectors.get(connectorId)?.chargingProfiles == null) {
399 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
400 connectors.get(connectorId)!.chargingProfiles = []
401 }
402 } else if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted == null) {
403 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
404 initializeConnectorStatus(connectors.get(connectorId)!)
405 }
406 }
407 }
408
409 export const resetAuthorizeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
410 connectorStatus.idTagLocalAuthorized = false
411 connectorStatus.idTagAuthorized = false
412 delete connectorStatus.localAuthorizeIdTag
413 delete connectorStatus.authorizeIdTag
414 }
415
416 export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefined): void => {
417 if (connectorStatus == null) {
418 return
419 }
420 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
421 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles.filter(
422 chargingProfile =>
423 chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE ||
424 (chargingProfile.transactionId != null &&
425 connectorStatus.transactionId != null &&
426 chargingProfile.transactionId !== connectorStatus.transactionId)
427 )
428 }
429 resetAuthorizeConnectorStatus(connectorStatus)
430 connectorStatus.transactionRemoteStarted = false
431 connectorStatus.transactionStarted = false
432 delete connectorStatus.transactionStart
433 delete connectorStatus.transactionId
434 delete connectorStatus.transactionIdTag
435 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
436 delete connectorStatus.transactionBeginMeterValue
437 }
438
439 export const prepareDatesInConnectorStatus = (
440 connectorStatus: ConnectorStatus
441 ): ConnectorStatus => {
442 if (connectorStatus.reservation != null) {
443 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
444 connectorStatus.reservation.expiryDate = convertToDate(connectorStatus.reservation.expiryDate)!
445 }
446 if (isNotEmptyArray(connectorStatus.chargingProfiles)) {
447 connectorStatus.chargingProfiles = connectorStatus.chargingProfiles.map(chargingProfile => {
448 chargingProfile.chargingSchedule.startSchedule = convertToDate(
449 chargingProfile.chargingSchedule.startSchedule
450 )
451 chargingProfile.validFrom = convertToDate(chargingProfile.validFrom)
452 chargingProfile.validTo = convertToDate(chargingProfile.validTo)
453 return chargingProfile
454 })
455 }
456 return connectorStatus
457 }
458
459 export const createBootNotificationRequest = (
460 stationInfo: ChargingStationInfo,
461 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
462 ): BootNotificationRequest | undefined => {
463 const ocppVersion = stationInfo.ocppVersion
464 switch (ocppVersion) {
465 case OCPPVersion.VERSION_16:
466 return {
467 chargePointModel: stationInfo.chargePointModel,
468 chargePointVendor: stationInfo.chargePointVendor,
469 ...(stationInfo.chargeBoxSerialNumber != null && {
470 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
471 }),
472 ...(stationInfo.chargePointSerialNumber != null && {
473 chargePointSerialNumber: stationInfo.chargePointSerialNumber
474 }),
475 ...(stationInfo.firmwareVersion != null && {
476 firmwareVersion: stationInfo.firmwareVersion
477 }),
478 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
479 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
480 ...(stationInfo.meterSerialNumber != null && {
481 meterSerialNumber: stationInfo.meterSerialNumber
482 }),
483 ...(stationInfo.meterType != null && {
484 meterType: stationInfo.meterType
485 })
486 } satisfies OCPP16BootNotificationRequest
487 case OCPPVersion.VERSION_20:
488 case OCPPVersion.VERSION_201:
489 return {
490 reason: bootReason,
491 chargingStation: {
492 model: stationInfo.chargePointModel,
493 vendorName: stationInfo.chargePointVendor,
494 ...(stationInfo.firmwareVersion != null && {
495 firmwareVersion: stationInfo.firmwareVersion
496 }),
497 ...(stationInfo.chargeBoxSerialNumber != null && {
498 serialNumber: stationInfo.chargeBoxSerialNumber
499 }),
500 ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
501 modem: {
502 ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
503 ...(stationInfo.imsi != null && { imsi: stationInfo.imsi })
504 }
505 })
506 }
507 } satisfies OCPP20BootNotificationRequest
508 }
509 }
510
511 export const warnTemplateKeysDeprecation = (
512 stationTemplate: ChargingStationTemplate,
513 logPrefix: string,
514 templateFile: string
515 ): void => {
516 const templateKeys: Array<{ deprecatedKey: string, key?: string }> = [
517 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
518 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
519 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
520 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' }
521 ]
522 for (const templateKey of templateKeys) {
523 warnDeprecatedTemplateKey(
524 stationTemplate,
525 templateKey.deprecatedKey,
526 logPrefix,
527 templateFile,
528 templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
529 )
530 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
531 }
532 }
533
534 export const stationTemplateToStationInfo = (
535 stationTemplate: ChargingStationTemplate
536 ): ChargingStationInfo => {
537 stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
538 delete stationTemplate.power
539 delete stationTemplate.powerUnit
540 delete stationTemplate.Connectors
541 delete stationTemplate.Evses
542 delete stationTemplate.Configuration
543 delete stationTemplate.AutomaticTransactionGenerator
544 delete stationTemplate.numberOfConnectors
545 delete stationTemplate.chargeBoxSerialNumberPrefix
546 delete stationTemplate.chargePointSerialNumberPrefix
547 delete stationTemplate.meterSerialNumberPrefix
548 return stationTemplate as ChargingStationInfo
549 }
550
551 export const createSerialNumber = (
552 stationTemplate: ChargingStationTemplate,
553 stationInfo: ChargingStationInfo,
554 params?: {
555 randomSerialNumberUpperCase?: boolean
556 randomSerialNumber?: boolean
557 }
558 ): void => {
559 params = {
560 ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true },
561 ...params
562 }
563 const serialNumberSuffix =
564 params.randomSerialNumber === true
565 ? getRandomSerialNumberSuffix({
566 upperCase: params.randomSerialNumberUpperCase
567 })
568 : ''
569 isNotEmptyString(stationTemplate.chargePointSerialNumberPrefix) &&
570 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`)
571 isNotEmptyString(stationTemplate.chargeBoxSerialNumberPrefix) &&
572 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`)
573 isNotEmptyString(stationTemplate.meterSerialNumberPrefix) &&
574 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`)
575 }
576
577 export const propagateSerialNumber = (
578 stationTemplate: ChargingStationTemplate | undefined,
579 stationInfoSrc: ChargingStationInfo | undefined,
580 stationInfoDst: ChargingStationInfo
581 ): void => {
582 if (stationInfoSrc == null || stationTemplate == null) {
583 throw new BaseError(
584 'Missing charging station template or existing configuration to propagate serial number'
585 )
586 }
587 stationTemplate.chargePointSerialNumberPrefix != null &&
588 stationInfoSrc.chargePointSerialNumber != null
589 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
590 : stationInfoDst.chargePointSerialNumber != null &&
591 delete stationInfoDst.chargePointSerialNumber
592 stationTemplate.chargeBoxSerialNumberPrefix != null &&
593 stationInfoSrc.chargeBoxSerialNumber != null
594 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
595 : stationInfoDst.chargeBoxSerialNumber != null && delete stationInfoDst.chargeBoxSerialNumber
596 stationTemplate.meterSerialNumberPrefix != null && stationInfoSrc.meterSerialNumber != null
597 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
598 : stationInfoDst.meterSerialNumber != null && delete stationInfoDst.meterSerialNumber
599 }
600
601 export const hasFeatureProfile = (
602 chargingStation: ChargingStation,
603 featureProfile: SupportedFeatureProfiles
604 ): boolean | undefined => {
605 return getConfigurationKey(
606 chargingStation,
607 StandardParametersKey.SupportedFeatureProfiles
608 )?.value?.includes(featureProfile)
609 }
610
611 export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
612 let unitDivider = 1
613 switch (stationInfo.amperageLimitationUnit) {
614 case AmpereUnits.DECI_AMPERE:
615 unitDivider = 10
616 break
617 case AmpereUnits.CENTI_AMPERE:
618 unitDivider = 100
619 break
620 case AmpereUnits.MILLI_AMPERE:
621 unitDivider = 1000
622 break
623 }
624 return unitDivider
625 }
626
627 /**
628 * Gets the connector cloned charging profiles applying a power limitation
629 * and sorted by connector id descending then stack level descending
630 *
631 * @param chargingStation -
632 * @param connectorId -
633 * @returns connector charging profiles array
634 */
635 export const getConnectorChargingProfiles = (
636 chargingStation: ChargingStation,
637 connectorId: number
638 ): ChargingProfile[] => {
639 return (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
640 .slice()
641 .sort((a, b) => b.stackLevel - a.stackLevel)
642 .concat(
643 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [])
644 .slice()
645 .sort((a, b) => b.stackLevel - a.stackLevel)
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 }