fix: allow to set charging profile with TxProfile purpose
[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 clone<ChargingProfile[]>(
640 (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
641 .sort((a, b) => b.stackLevel - a.stackLevel)
642 .concat(
643 (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
644 (a, b) => b.stackLevel - a.stackLevel
645 )
646 )
647 )
648 }
649
650 export const getChargingStationConnectorChargingProfilesPowerLimit = (
651 chargingStation: ChargingStation,
652 connectorId: number
653 ): number | undefined => {
654 let limit: number | undefined, chargingProfile: ChargingProfile | undefined
655 // Get charging profiles sorted by connector id then stack level
656 const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId)
657 if (isNotEmptyArray(chargingProfiles)) {
658 const result = getLimitFromChargingProfiles(
659 chargingStation,
660 connectorId,
661 chargingProfiles,
662 chargingStation.logPrefix()
663 )
664 if (result != null) {
665 limit = result.limit
666 chargingProfile = result.chargingProfile
667 switch (chargingStation.stationInfo?.currentOutType) {
668 case CurrentType.AC:
669 limit =
670 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
671 ? limit
672 : ACElectricUtils.powerTotal(
673 chargingStation.getNumberOfPhases(),
674 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
675 chargingStation.stationInfo.voltageOut!,
676 limit
677 )
678 break
679 case CurrentType.DC:
680 limit =
681 chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
682 ? limit
683 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
684 DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit)
685 }
686 const connectorMaximumPower =
687 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
688 chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
689 if (limit > connectorMaximumPower) {
690 logger.error(
691 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${
692 chargingProfile.chargingProfileId
693 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
694 result
695 )
696 limit = connectorMaximumPower
697 }
698 }
699 }
700 return limit
701 }
702
703 export const getDefaultVoltageOut = (
704 currentType: CurrentType,
705 logPrefix: string,
706 templateFile: string
707 ): Voltage => {
708 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
709 let defaultVoltageOut: number
710 switch (currentType) {
711 case CurrentType.AC:
712 defaultVoltageOut = Voltage.VOLTAGE_230
713 break
714 case CurrentType.DC:
715 defaultVoltageOut = Voltage.VOLTAGE_400
716 break
717 default:
718 logger.error(`${logPrefix} ${errorMsg}`)
719 throw new BaseError(errorMsg)
720 }
721 return defaultVoltageOut
722 }
723
724 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
725 return stationInfo.idTagsFile != null
726 ? join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
727 : undefined
728 }
729
730 export const waitChargingStationEvents = async (
731 emitter: EventEmitter,
732 event: ChargingStationWorkerMessageEvents,
733 eventsToWait: number
734 ): Promise<number> => {
735 return await new Promise<number>(resolve => {
736 let events = 0
737 if (eventsToWait === 0) {
738 resolve(events)
739 return
740 }
741 emitter.on(event, () => {
742 ++events
743 if (events === eventsToWait) {
744 resolve(events)
745 }
746 })
747 })
748 }
749
750 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
751 let configuredMaxNumberOfConnectors = 0
752 if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
753 const numberOfConnectors = stationTemplate.numberOfConnectors
754 configuredMaxNumberOfConnectors =
755 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
756 } else if (stationTemplate.numberOfConnectors != null) {
757 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
758 } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
759 configuredMaxNumberOfConnectors =
760 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
761 stationTemplate.Connectors[0] != null
762 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
763 : getMaxNumberOfConnectors(stationTemplate.Connectors)
764 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
765 for (const evse in stationTemplate.Evses) {
766 if (evse === '0') {
767 continue
768 }
769 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
770 stationTemplate.Evses[evse].Connectors
771 )
772 }
773 }
774 return configuredMaxNumberOfConnectors
775 }
776
777 const checkConfiguredMaxConnectors = (
778 configuredMaxConnectors: number,
779 logPrefix: string,
780 templateFile: string
781 ): void => {
782 if (configuredMaxConnectors <= 0) {
783 logger.warn(
784 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
785 )
786 }
787 }
788
789 const checkTemplateMaxConnectors = (
790 templateMaxConnectors: number,
791 logPrefix: string,
792 templateFile: string
793 ): void => {
794 if (templateMaxConnectors === 0) {
795 logger.warn(
796 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
797 )
798 } else if (templateMaxConnectors < 0) {
799 logger.error(
800 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
801 )
802 }
803 }
804
805 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
806 connectorStatus.availability = AvailabilityType.Operative
807 connectorStatus.idTagLocalAuthorized = false
808 connectorStatus.idTagAuthorized = false
809 connectorStatus.transactionRemoteStarted = false
810 connectorStatus.transactionStarted = false
811 connectorStatus.energyActiveImportRegisterValue = 0
812 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
813 if (connectorStatus.chargingProfiles == null) {
814 connectorStatus.chargingProfiles = []
815 }
816 }
817
818 const warnDeprecatedTemplateKey = (
819 template: ChargingStationTemplate,
820 key: string,
821 logPrefix: string,
822 templateFile: string,
823 logMsgToAppend = ''
824 ): void => {
825 if (template[key as keyof ChargingStationTemplate] != null) {
826 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
827 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
828 }`
829 logger.warn(`${logPrefix} ${logMsg}`)
830 console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`)
831 }
832 }
833
834 const convertDeprecatedTemplateKey = (
835 template: ChargingStationTemplate,
836 deprecatedKey: string,
837 key?: string
838 ): void => {
839 if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
840 if (key != null) {
841 (template as unknown as Record<string, unknown>)[key] =
842 template[deprecatedKey as keyof ChargingStationTemplate]
843 }
844 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
845 delete template[deprecatedKey as keyof ChargingStationTemplate]
846 }
847 }
848
849 interface ChargingProfilesLimit {
850 limit: number
851 chargingProfile: ChargingProfile
852 }
853
854 /**
855 * Charging profiles shall already be sorted by connector id descending then stack level descending
856 *
857 * @param chargingStation -
858 * @param connectorId -
859 * @param chargingProfiles -
860 * @param logPrefix -
861 * @returns ChargingProfilesLimit
862 */
863 const getLimitFromChargingProfiles = (
864 chargingStation: ChargingStation,
865 connectorId: number,
866 chargingProfiles: ChargingProfile[],
867 logPrefix: string
868 ): ChargingProfilesLimit | undefined => {
869 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
870 const currentDate = new Date()
871 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
872 for (const chargingProfile of chargingProfiles) {
873 const chargingSchedule = chargingProfile.chargingSchedule
874 if (chargingSchedule.startSchedule == null) {
875 logger.debug(
876 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
877 )
878 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
879 chargingSchedule.startSchedule = connectorStatus?.transactionStart
880 }
881 if (!isDate(chargingSchedule.startSchedule)) {
882 logger.warn(
883 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
884 )
885 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
886 chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
887 }
888 if (chargingSchedule.duration == null) {
889 logger.debug(
890 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
891 )
892 // OCPP specifies that if duration is not defined, it should be infinite
893 chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
894 }
895 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
896 continue
897 }
898 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
899 continue
900 }
901 // Check if the charging profile is active
902 if (
903 isWithinInterval(currentDate, {
904 start: chargingSchedule.startSchedule,
905 end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
906 })
907 ) {
908 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
909 const chargingSchedulePeriodCompareFn = (
910 a: ChargingSchedulePeriod,
911 b: ChargingSchedulePeriod
912 ): number => a.startPeriod - b.startPeriod
913 if (
914 !isArraySorted<ChargingSchedulePeriod>(
915 chargingSchedule.chargingSchedulePeriod,
916 chargingSchedulePeriodCompareFn
917 )
918 ) {
919 logger.warn(
920 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`
921 )
922 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
923 }
924 // Check if the first schedule period startPeriod property is equal to 0
925 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
926 logger.error(
927 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`
928 )
929 continue
930 }
931 // Handle only one schedule period
932 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
933 const result: ChargingProfilesLimit = {
934 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
935 chargingProfile
936 }
937 logger.debug(debugLogMsg, result)
938 return result
939 }
940 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined
941 // Search for the right schedule period
942 for (const [
943 index,
944 chargingSchedulePeriod
945 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
946 // Find the right schedule period
947 if (
948 isAfter(
949 addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
950 currentDate
951 )
952 ) {
953 // Found the schedule period: previous is the correct one
954 const result: ChargingProfilesLimit = {
955 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
956 limit: previousChargingSchedulePeriod!.limit,
957 chargingProfile
958 }
959 logger.debug(debugLogMsg, result)
960 return result
961 }
962 // Keep a reference to previous one
963 previousChargingSchedulePeriod = chargingSchedulePeriod
964 // Handle the last schedule period within the charging profile duration
965 if (
966 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
967 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
968 differenceInSeconds(
969 addSeconds(
970 chargingSchedule.startSchedule,
971 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
972 ),
973 chargingSchedule.startSchedule
974 ) > chargingSchedule.duration)
975 ) {
976 const result: ChargingProfilesLimit = {
977 limit: previousChargingSchedulePeriod.limit,
978 chargingProfile
979 }
980 logger.debug(debugLogMsg, result)
981 return result
982 }
983 }
984 }
985 }
986 }
987 }
988
989 export const prepareChargingProfileKind = (
990 connectorStatus: ConnectorStatus | undefined,
991 chargingProfile: ChargingProfile,
992 currentDate: string | number | Date,
993 logPrefix: string
994 ): boolean => {
995 switch (chargingProfile.chargingProfileKind) {
996 case ChargingProfileKindType.RECURRING:
997 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
998 return false
999 }
1000 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix)
1001 break
1002 case ChargingProfileKindType.RELATIVE:
1003 if (chargingProfile.chargingSchedule.startSchedule != null) {
1004 logger.warn(
1005 `${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`
1006 )
1007 delete chargingProfile.chargingSchedule.startSchedule
1008 }
1009 if (connectorStatus?.transactionStarted === true) {
1010 chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
1011 }
1012 // FIXME: handle relative charging profile duration
1013 break
1014 }
1015 return true
1016 }
1017
1018 export const canProceedChargingProfile = (
1019 chargingProfile: ChargingProfile,
1020 currentDate: string | number | Date,
1021 logPrefix: string
1022 ): boolean => {
1023 if (
1024 (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
1025 (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
1026 ) {
1027 logger.debug(
1028 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
1029 chargingProfile.chargingProfileId
1030 } is not valid for the current date ${
1031 isDate(currentDate) ? currentDate.toISOString() : currentDate
1032 }`
1033 )
1034 return false
1035 }
1036 if (
1037 chargingProfile.chargingSchedule.startSchedule == null ||
1038 chargingProfile.chargingSchedule.duration == null
1039 ) {
1040 logger.error(
1041 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`
1042 )
1043 return false
1044 }
1045 if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
1046 logger.error(
1047 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
1048 )
1049 return false
1050 }
1051 if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
1052 logger.error(
1053 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`
1054 )
1055 return false
1056 }
1057 return true
1058 }
1059
1060 const canProceedRecurringChargingProfile = (
1061 chargingProfile: ChargingProfile,
1062 logPrefix: string
1063 ): boolean => {
1064 if (
1065 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1066 chargingProfile.recurrencyKind == null
1067 ) {
1068 logger.error(
1069 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`
1070 )
1071 return false
1072 }
1073 if (
1074 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
1075 chargingProfile.chargingSchedule.startSchedule == null
1076 ) {
1077 logger.error(
1078 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`
1079 )
1080 return false
1081 }
1082 return true
1083 }
1084
1085 /**
1086 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
1087 *
1088 * @param chargingProfile -
1089 * @param currentDate -
1090 * @param logPrefix -
1091 */
1092 const prepareRecurringChargingProfile = (
1093 chargingProfile: ChargingProfile,
1094 currentDate: string | number | Date,
1095 logPrefix: string
1096 ): boolean => {
1097 const chargingSchedule = chargingProfile.chargingSchedule
1098 let recurringIntervalTranslated = false
1099 let recurringInterval: Interval | undefined
1100 switch (chargingProfile.recurrencyKind) {
1101 case RecurrencyKindType.DAILY:
1102 recurringInterval = {
1103 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1104 start: chargingSchedule.startSchedule!,
1105 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1106 end: addDays(chargingSchedule.startSchedule!, 1)
1107 }
1108 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1109 if (
1110 !isWithinInterval(currentDate, recurringInterval) &&
1111 isBefore(recurringInterval.end, currentDate)
1112 ) {
1113 chargingSchedule.startSchedule = addDays(
1114 recurringInterval.start,
1115 differenceInDays(currentDate, recurringInterval.start)
1116 )
1117 recurringInterval = {
1118 start: chargingSchedule.startSchedule,
1119 end: addDays(chargingSchedule.startSchedule, 1)
1120 }
1121 recurringIntervalTranslated = true
1122 }
1123 break
1124 case RecurrencyKindType.WEEKLY:
1125 recurringInterval = {
1126 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1127 start: chargingSchedule.startSchedule!,
1128 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1129 end: addWeeks(chargingSchedule.startSchedule!, 1)
1130 }
1131 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
1132 if (
1133 !isWithinInterval(currentDate, recurringInterval) &&
1134 isBefore(recurringInterval.end, currentDate)
1135 ) {
1136 chargingSchedule.startSchedule = addWeeks(
1137 recurringInterval.start,
1138 differenceInWeeks(currentDate, recurringInterval.start)
1139 )
1140 recurringInterval = {
1141 start: chargingSchedule.startSchedule,
1142 end: addWeeks(chargingSchedule.startSchedule, 1)
1143 }
1144 recurringIntervalTranslated = true
1145 }
1146 break
1147 default:
1148 logger.error(
1149 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`
1150 )
1151 }
1152 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1153 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
1154 logger.error(
1155 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
1156 chargingProfile.recurrencyKind
1157 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
1158 recurringInterval?.start as Date
1159 ).toISOString()}, ${toDate(
1160 recurringInterval?.end as Date
1161 ).toISOString()}] has not been properly translated to current date ${
1162 isDate(currentDate) ? currentDate.toISOString() : currentDate
1163 } `
1164 )
1165 }
1166 return recurringIntervalTranslated
1167 }
1168
1169 const checkRecurringChargingProfileDuration = (
1170 chargingProfile: ChargingProfile,
1171 interval: Interval,
1172 logPrefix: string
1173 ): void => {
1174 if (chargingProfile.chargingSchedule.duration == null) {
1175 logger.warn(
1176 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1177 chargingProfile.chargingProfileKind
1178 } charging profile id ${
1179 chargingProfile.chargingProfileId
1180 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1181 interval.end,
1182 interval.start
1183 )}`
1184 )
1185 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1186 } else if (
1187 chargingProfile.chargingSchedule.duration > differenceInSeconds(interval.end, interval.start)
1188 ) {
1189 logger.warn(
1190 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1191 chargingProfile.chargingProfileKind
1192 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1193 chargingProfile.chargingSchedule.duration
1194 } is greater than the recurrency time interval duration ${differenceInSeconds(
1195 interval.end,
1196 interval.start
1197 )}`
1198 )
1199 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start)
1200 }
1201 }
1202
1203 const getRandomSerialNumberSuffix = (params?: {
1204 randomBytesLength?: number
1205 upperCase?: boolean
1206 }): string => {
1207 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex')
1208 if (params?.upperCase === true) {
1209 return randomSerialNumberSuffix.toUpperCase()
1210 }
1211 return randomSerialNumberSuffix
1212 }