272abc8cb024f78a9b1dbcae445a4f7043e5720c
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
2
3 import { createHash, randomInt } from 'node:crypto'
4 import { EventEmitter } from 'node:events'
5 import { existsSync, type FSWatcher, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
6 import { dirname, join } from 'node:path'
7 import { URL } from 'node:url'
8 import { parentPort } from 'node:worker_threads'
9
10 import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns'
11 import { mergeDeepRight, once } from 'rambda'
12 import { type RawData, WebSocket } from 'ws'
13
14 import { BaseError, OCPPError } from '../exception/index.js'
15 import { PerformanceStatistics } from '../performance/index.js'
16 import {
17 type AutomaticTransactionGeneratorConfiguration,
18 AvailabilityType,
19 type BootNotificationRequest,
20 type BootNotificationResponse,
21 type CachedRequest,
22 type ChargingStationConfiguration,
23 ChargingStationEvents,
24 type ChargingStationInfo,
25 type ChargingStationOcppConfiguration,
26 type ChargingStationOptions,
27 type ChargingStationTemplate,
28 type ConnectorStatus,
29 ConnectorStatusEnum,
30 CurrentType,
31 type ErrorCallback,
32 type ErrorResponse,
33 ErrorType,
34 type EvseStatus,
35 type EvseStatusConfiguration,
36 FileType,
37 FirmwareStatus,
38 type FirmwareStatusNotificationRequest,
39 type FirmwareStatusNotificationResponse,
40 type HeartbeatRequest,
41 type HeartbeatResponse,
42 type IncomingRequest,
43 type IncomingRequestCommand,
44 MessageType,
45 MeterValueMeasurand,
46 type MeterValuesRequest,
47 type MeterValuesResponse,
48 OCPPVersion,
49 type OutgoingRequest,
50 PowerUnits,
51 RegistrationStatusEnumType,
52 RequestCommand,
53 type Reservation,
54 type ReservationKey,
55 ReservationTerminationReason,
56 type Response,
57 StandardParametersKey,
58 type Status,
59 type StopTransactionReason,
60 type StopTransactionRequest,
61 type StopTransactionResponse,
62 SupervisionUrlDistribution,
63 SupportedFeatureProfiles,
64 type Voltage,
65 WebSocketCloseEventStatusCode,
66 type WSError,
67 type WsOptions
68 } from '../types/index.js'
69 import {
70 ACElectricUtils,
71 AsyncLock,
72 AsyncLockType,
73 buildAddedMessage,
74 buildChargingStationAutomaticTransactionGeneratorConfiguration,
75 buildConnectorsStatus,
76 buildDeletedMessage,
77 buildEvsesStatus,
78 buildStartedMessage,
79 buildStoppedMessage,
80 buildUpdatedMessage,
81 clone,
82 Configuration,
83 Constants,
84 convertToBoolean,
85 convertToDate,
86 convertToInt,
87 DCElectricUtils,
88 exponentialDelay,
89 formatDurationMilliSeconds,
90 formatDurationSeconds,
91 getWebSocketCloseEventStatusString,
92 handleFileException,
93 isNotEmptyArray,
94 isNotEmptyString,
95 logger,
96 logPrefix,
97 min,
98 roundTo,
99 secureRandom,
100 sleep,
101 watchJsonFile
102 } from '../utils/index.js'
103 import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js'
104 import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel.js'
105 import {
106 addConfigurationKey,
107 deleteConfigurationKey,
108 getConfigurationKey,
109 setConfigurationKeyValue
110 } from './ConfigurationKeyUtils.js'
111 import {
112 buildConnectorsMap,
113 buildTemplateName,
114 checkChargingStation,
115 checkConfiguration,
116 checkConnectorsConfiguration,
117 checkStationInfoConnectorStatus,
118 checkTemplate,
119 createBootNotificationRequest,
120 createSerialNumber,
121 getAmperageLimitationUnitDivider,
122 getBootConnectorStatus,
123 getChargingStationConnectorChargingProfilesPowerLimit,
124 getChargingStationId,
125 getDefaultVoltageOut,
126 getHashId,
127 getIdTagsFile,
128 getMaxNumberOfEvses,
129 getNumberOfReservableConnectors,
130 getPhaseRotationValue,
131 hasFeatureProfile,
132 hasReservationExpired,
133 initializeConnectorsMapStatus,
134 propagateSerialNumber,
135 setChargingStationOptions,
136 stationTemplateToStationInfo,
137 warnTemplateKeysDeprecation
138 } from './Helpers.js'
139 import { IdTagsCache } from './IdTagsCache.js'
140 import {
141 buildMeterValue,
142 buildTransactionEndMeterValue,
143 getMessageTypeString,
144 OCPP16IncomingRequestService,
145 OCPP16RequestService,
146 OCPP16ResponseService,
147 OCPP20IncomingRequestService,
148 OCPP20RequestService,
149 OCPP20ResponseService,
150 type OCPPIncomingRequestService,
151 type OCPPRequestService,
152 sendAndSetConnectorStatus
153 } from './ocpp/index.js'
154 import { SharedLRUCache } from './SharedLRUCache.js'
155
156 export class ChargingStation extends EventEmitter {
157 public readonly index: number
158 public readonly templateFile: string
159 public stationInfo?: ChargingStationInfo
160 public started: boolean
161 public starting: boolean
162 public idTagsCache: IdTagsCache
163 public automaticTransactionGenerator?: AutomaticTransactionGenerator
164 public ocppConfiguration?: ChargingStationOcppConfiguration
165 public wsConnection: WebSocket | null
166 public readonly connectors: Map<number, ConnectorStatus>
167 public readonly evses: Map<number, EvseStatus>
168 public readonly requests: Map<string, CachedRequest>
169 public performanceStatistics?: PerformanceStatistics
170 public heartbeatSetInterval?: NodeJS.Timeout
171 public ocppRequestService!: OCPPRequestService
172 public bootNotificationRequest?: BootNotificationRequest
173 public bootNotificationResponse?: BootNotificationResponse
174 public powerDivider?: number
175 private stopping: boolean
176 private configurationFile!: string
177 private configurationFileHash!: string
178 private connectorsConfigurationHash!: string
179 private evsesConfigurationHash!: string
180 private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
181 private ocppIncomingRequestService!: OCPPIncomingRequestService
182 private readonly messageBuffer: Set<string>
183 private configuredSupervisionUrl!: URL
184 private wsConnectionRetried: boolean
185 private wsConnectionRetryCount: number
186 private templateFileWatcher?: FSWatcher
187 private templateFileHash!: string
188 private readonly sharedLRUCache: SharedLRUCache
189 private wsPingSetInterval?: NodeJS.Timeout
190 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
191 private flushMessageBufferSetInterval?: NodeJS.Timeout
192
193 constructor (index: number, templateFile: string, options?: ChargingStationOptions) {
194 super()
195 this.started = false
196 this.starting = false
197 this.stopping = false
198 this.wsConnection = null
199 this.wsConnectionRetried = false
200 this.wsConnectionRetryCount = 0
201 this.index = index
202 this.templateFile = templateFile
203 this.connectors = new Map<number, ConnectorStatus>()
204 this.evses = new Map<number, EvseStatus>()
205 this.requests = new Map<string, CachedRequest>()
206 this.messageBuffer = new Set<string>()
207 this.sharedLRUCache = SharedLRUCache.getInstance()
208 this.idTagsCache = IdTagsCache.getInstance()
209 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this)
210
211 this.on(ChargingStationEvents.added, () => {
212 parentPort?.postMessage(buildAddedMessage(this))
213 })
214 this.on(ChargingStationEvents.deleted, () => {
215 parentPort?.postMessage(buildDeletedMessage(this))
216 })
217 this.on(ChargingStationEvents.started, () => {
218 parentPort?.postMessage(buildStartedMessage(this))
219 })
220 this.on(ChargingStationEvents.stopped, () => {
221 parentPort?.postMessage(buildStoppedMessage(this))
222 })
223 this.on(ChargingStationEvents.updated, () => {
224 parentPort?.postMessage(buildUpdatedMessage(this))
225 })
226 this.on(ChargingStationEvents.accepted, () => {
227 this.startMessageSequence(
228 this.wsConnectionRetried
229 ? true
230 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
231 ).catch((error: unknown) => {
232 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
233 })
234 this.wsConnectionRetried = false
235 })
236 this.on(ChargingStationEvents.rejected, () => {
237 this.wsConnectionRetried = false
238 })
239 this.on(ChargingStationEvents.disconnected, () => {
240 try {
241 this.internalStopMessageSequence()
242 } catch (error) {
243 logger.error(
244 `${this.logPrefix()} Error while stopping the internal message sequence:`,
245 error
246 )
247 }
248 })
249
250 this.initialize(options)
251
252 this.add()
253
254 if (this.stationInfo?.autoStart === true) {
255 this.start()
256 }
257 }
258
259 public get hasEvses (): boolean {
260 return this.connectors.size === 0 && this.evses.size > 0
261 }
262
263 public get wsConnectionUrl (): URL {
264 return new URL(
265 `${
266 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
267 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
268 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
269 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
270 : this.configuredSupervisionUrl.href
271 }/${this.stationInfo?.chargingStationId}`
272 )
273 }
274
275 public logPrefix = (): string => {
276 if (
277 this instanceof ChargingStation &&
278 this.stationInfo != null &&
279 isNotEmptyString(this.stationInfo.chargingStationId)
280 ) {
281 return logPrefix(` ${this.stationInfo.chargingStationId} |`)
282 }
283 let stationTemplate: ChargingStationTemplate | undefined
284 try {
285 stationTemplate = JSON.parse(
286 readFileSync(this.templateFile, 'utf8')
287 ) as ChargingStationTemplate
288 } catch {
289 stationTemplate = undefined
290 }
291 return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
292 }
293
294 public hasIdTags (): boolean {
295 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296 return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo!)!))
297 }
298
299 public getNumberOfPhases (stationInfo?: ChargingStationInfo): number {
300 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
301 const localStationInfo = stationInfo ?? this.stationInfo!
302 switch (this.getCurrentOutType(stationInfo)) {
303 case CurrentType.AC:
304 return localStationInfo.numberOfPhases ?? 3
305 case CurrentType.DC:
306 return 0
307 }
308 }
309
310 public isWebSocketConnectionOpened (): boolean {
311 return this.wsConnection?.readyState === WebSocket.OPEN
312 }
313
314 public inUnknownState (): boolean {
315 return this.bootNotificationResponse?.status == null
316 }
317
318 public inPendingState (): boolean {
319 return this.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING
320 }
321
322 public inAcceptedState (): boolean {
323 return this.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED
324 }
325
326 public inRejectedState (): boolean {
327 return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED
328 }
329
330 public isRegistered (): boolean {
331 return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
332 }
333
334 public isChargingStationAvailable (): boolean {
335 return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative
336 }
337
338 public hasConnector (connectorId: number): boolean {
339 if (this.hasEvses) {
340 for (const evseStatus of this.evses.values()) {
341 if (evseStatus.connectors.has(connectorId)) {
342 return true
343 }
344 }
345 return false
346 }
347 return this.connectors.has(connectorId)
348 }
349
350 public isConnectorAvailable (connectorId: number): boolean {
351 return (
352 connectorId > 0 &&
353 this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative
354 )
355 }
356
357 public getNumberOfConnectors (): number {
358 if (this.hasEvses) {
359 let numberOfConnectors = 0
360 for (const [evseId, evseStatus] of this.evses) {
361 if (evseId > 0) {
362 numberOfConnectors += evseStatus.connectors.size
363 }
364 }
365 return numberOfConnectors
366 }
367 return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size
368 }
369
370 public getNumberOfEvses (): number {
371 return this.evses.has(0) ? this.evses.size - 1 : this.evses.size
372 }
373
374 public getConnectorStatus (connectorId: number): ConnectorStatus | undefined {
375 if (this.hasEvses) {
376 for (const evseStatus of this.evses.values()) {
377 if (evseStatus.connectors.has(connectorId)) {
378 return evseStatus.connectors.get(connectorId)
379 }
380 }
381 return undefined
382 }
383 return this.connectors.get(connectorId)
384 }
385
386 public getConnectorMaximumAvailablePower (connectorId: number): number {
387 let connectorAmperageLimitationPowerLimit: number | undefined
388 const amperageLimitation = this.getAmperageLimitation()
389 if (
390 amperageLimitation != null &&
391 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
392 amperageLimitation < this.stationInfo!.maximumAmperage!
393 ) {
394 connectorAmperageLimitationPowerLimit =
395 (this.stationInfo?.currentOutType === CurrentType.AC
396 ? ACElectricUtils.powerTotal(
397 this.getNumberOfPhases(),
398 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
399 this.stationInfo.voltageOut!,
400 amperageLimitation *
401 (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors())
402 )
403 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
404 DCElectricUtils.power(this.stationInfo!.voltageOut!, amperageLimitation)) /
405 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
406 this.powerDivider!
407 }
408 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
409 const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider!
410 const connectorChargingProfilesPowerLimit =
411 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId)
412 return min(
413 isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
414 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
415 isNaN(connectorAmperageLimitationPowerLimit!)
416 ? Infinity
417 : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
418 connectorAmperageLimitationPowerLimit!,
419 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
420 isNaN(connectorChargingProfilesPowerLimit!) ? Infinity : connectorChargingProfilesPowerLimit!
421 )
422 }
423
424 public getTransactionIdTag (transactionId: number): string | undefined {
425 if (this.hasEvses) {
426 for (const evseStatus of this.evses.values()) {
427 for (const connectorStatus of evseStatus.connectors.values()) {
428 if (connectorStatus.transactionId === transactionId) {
429 return connectorStatus.transactionIdTag
430 }
431 }
432 }
433 } else {
434 for (const connectorId of this.connectors.keys()) {
435 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
436 return this.getConnectorStatus(connectorId)?.transactionIdTag
437 }
438 }
439 }
440 }
441
442 public getNumberOfRunningTransactions (): number {
443 let numberOfRunningTransactions = 0
444 if (this.hasEvses) {
445 for (const [evseId, evseStatus] of this.evses) {
446 if (evseId === 0) {
447 continue
448 }
449 for (const connectorStatus of evseStatus.connectors.values()) {
450 if (connectorStatus.transactionStarted === true) {
451 ++numberOfRunningTransactions
452 }
453 }
454 }
455 } else {
456 for (const connectorId of this.connectors.keys()) {
457 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
458 ++numberOfRunningTransactions
459 }
460 }
461 }
462 return numberOfRunningTransactions
463 }
464
465 public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined {
466 if (transactionId == null) {
467 return undefined
468 } else if (this.hasEvses) {
469 for (const evseStatus of this.evses.values()) {
470 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
471 if (connectorStatus.transactionId === transactionId) {
472 return connectorId
473 }
474 }
475 }
476 } else {
477 for (const connectorId of this.connectors.keys()) {
478 if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
479 return connectorId
480 }
481 }
482 }
483 }
484
485 public getEnergyActiveImportRegisterByTransactionId (
486 transactionId: number | undefined,
487 rounded = false
488 ): number {
489 return this.getEnergyActiveImportRegister(
490 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
491 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!),
492 rounded
493 )
494 }
495
496 public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number {
497 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded)
498 }
499
500 public getAuthorizeRemoteTxRequests (): boolean {
501 const authorizeRemoteTxRequests = getConfigurationKey(
502 this,
503 StandardParametersKey.AuthorizeRemoteTxRequests
504 )
505 return authorizeRemoteTxRequests != null
506 ? convertToBoolean(authorizeRemoteTxRequests.value)
507 : false
508 }
509
510 public getLocalAuthListEnabled (): boolean {
511 const localAuthListEnabled = getConfigurationKey(
512 this,
513 StandardParametersKey.LocalAuthListEnabled
514 )
515 return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false
516 }
517
518 public getHeartbeatInterval (): number {
519 const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)
520 if (HeartbeatInterval != null) {
521 return secondsToMilliseconds(convertToInt(HeartbeatInterval.value))
522 }
523 const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)
524 if (HeartBeatInterval != null) {
525 return secondsToMilliseconds(convertToInt(HeartBeatInterval.value))
526 }
527 this.stationInfo?.autoRegister === false &&
528 logger.warn(
529 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
530 Constants.DEFAULT_HEARTBEAT_INTERVAL
531 }`
532 )
533 return Constants.DEFAULT_HEARTBEAT_INTERVAL
534 }
535
536 public setSupervisionUrl (url: string): void {
537 if (
538 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
539 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey)
540 ) {
541 setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
542 } else {
543 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
544 this.stationInfo!.supervisionUrls = url
545 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
546 this.saveStationInfo()
547 }
548 }
549
550 public startHeartbeat (): void {
551 if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval == null) {
552 this.heartbeatSetInterval = setInterval(() => {
553 this.ocppRequestService
554 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
555 .catch((error: unknown) => {
556 logger.error(
557 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
558 error
559 )
560 })
561 }, this.getHeartbeatInterval())
562 logger.info(
563 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
564 this.getHeartbeatInterval()
565 )}`
566 )
567 } else if (this.heartbeatSetInterval != null) {
568 logger.info(
569 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
570 this.getHeartbeatInterval()
571 )}`
572 )
573 } else {
574 logger.error(
575 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
576 )
577 }
578 }
579
580 public restartHeartbeat (): void {
581 // Stop heartbeat
582 this.stopHeartbeat()
583 // Start heartbeat
584 this.startHeartbeat()
585 }
586
587 public restartWebSocketPing (): void {
588 // Stop WebSocket ping
589 this.stopWebSocketPing()
590 // Start WebSocket ping
591 this.startWebSocketPing()
592 }
593
594 public startMeterValues (connectorId: number, interval: number): void {
595 if (connectorId === 0) {
596 logger.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
597 return
598 }
599 const connectorStatus = this.getConnectorStatus(connectorId)
600 if (connectorStatus == null) {
601 logger.error(
602 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
603 ${connectorId}`
604 )
605 return
606 }
607 if (connectorStatus.transactionStarted === false) {
608 logger.error(
609 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
610 )
611 return
612 } else if (
613 connectorStatus.transactionStarted === true &&
614 connectorStatus.transactionId == null
615 ) {
616 logger.error(
617 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
618 )
619 return
620 }
621 if (interval > 0) {
622 connectorStatus.transactionSetInterval = setInterval(() => {
623 const meterValue = buildMeterValue(
624 this,
625 connectorId,
626 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
627 connectorStatus.transactionId!,
628 interval
629 )
630 this.ocppRequestService
631 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
632 this,
633 RequestCommand.METER_VALUES,
634 {
635 connectorId,
636 transactionId: connectorStatus.transactionId,
637 meterValue: [meterValue]
638 }
639 )
640 .catch((error: unknown) => {
641 logger.error(
642 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
643 error
644 )
645 })
646 }, interval)
647 } else {
648 logger.error(
649 `${this.logPrefix()} Charging station ${
650 StandardParametersKey.MeterValueSampleInterval
651 } configuration set to ${interval}, not sending MeterValues`
652 )
653 }
654 }
655
656 public stopMeterValues (connectorId: number): void {
657 const connectorStatus = this.getConnectorStatus(connectorId)
658 if (connectorStatus?.transactionSetInterval != null) {
659 clearInterval(connectorStatus.transactionSetInterval)
660 }
661 }
662
663 private add (): void {
664 this.emit(ChargingStationEvents.added)
665 }
666
667 public async delete (deleteConfiguration = true): Promise<void> {
668 if (this.started) {
669 await this.stop()
670 }
671 AutomaticTransactionGenerator.deleteInstance(this)
672 PerformanceStatistics.deleteInstance(this.stationInfo?.hashId)
673 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
674 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
675 this.requests.clear()
676 this.connectors.clear()
677 this.evses.clear()
678 this.templateFileWatcher?.unref()
679 deleteConfiguration && rmSync(this.configurationFile, { force: true })
680 this.chargingStationWorkerBroadcastChannel.unref()
681 this.emit(ChargingStationEvents.deleted)
682 this.removeAllListeners()
683 }
684
685 public start (): void {
686 if (!this.started) {
687 if (!this.starting) {
688 this.starting = true
689 if (this.stationInfo?.enableStatistics === true) {
690 this.performanceStatistics?.start()
691 }
692 this.openWSConnection()
693 // Monitor charging station template file
694 this.templateFileWatcher = watchJsonFile(
695 this.templateFile,
696 FileType.ChargingStationTemplate,
697 this.logPrefix(),
698 undefined,
699 (event, filename): void => {
700 if (isNotEmptyString(filename) && event === 'change') {
701 try {
702 logger.debug(
703 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
704 this.templateFile
705 } file have changed, reload`
706 )
707 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
708 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
709 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
710 // Initialize
711 this.initialize()
712 // Restart the ATG
713 const ATGStarted = this.automaticTransactionGenerator?.started
714 if (ATGStarted === true) {
715 this.stopAutomaticTransactionGenerator()
716 }
717 delete this.automaticTransactionGeneratorConfiguration
718 if (
719 this.getAutomaticTransactionGeneratorConfiguration()?.enable === true &&
720 ATGStarted === true
721 ) {
722 this.startAutomaticTransactionGenerator(undefined, true)
723 }
724 if (this.stationInfo?.enableStatistics === true) {
725 this.performanceStatistics?.restart()
726 } else {
727 this.performanceStatistics?.stop()
728 }
729 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
730 } catch (error) {
731 logger.error(
732 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
733 error
734 )
735 }
736 }
737 }
738 )
739 this.started = true
740 this.emit(ChargingStationEvents.started)
741 this.starting = false
742 } else {
743 logger.warn(`${this.logPrefix()} Charging station is already starting...`)
744 }
745 } else {
746 logger.warn(`${this.logPrefix()} Charging station is already started...`)
747 }
748 }
749
750 public async stop (
751 reason?: StopTransactionReason,
752 stopTransactions = this.stationInfo?.stopTransactionsOnStopped
753 ): Promise<void> {
754 if (this.started) {
755 if (!this.stopping) {
756 this.stopping = true
757 await this.stopMessageSequence(reason, stopTransactions)
758 this.closeWSConnection()
759 if (this.stationInfo?.enableStatistics === true) {
760 this.performanceStatistics?.stop()
761 }
762 this.templateFileWatcher?.close()
763 delete this.bootNotificationResponse
764 this.started = false
765 this.saveConfiguration()
766 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
767 this.emit(ChargingStationEvents.stopped)
768 this.stopping = false
769 } else {
770 logger.warn(`${this.logPrefix()} Charging station is already stopping...`)
771 }
772 } else {
773 logger.warn(`${this.logPrefix()} Charging station is already stopped...`)
774 }
775 }
776
777 public async reset (reason?: StopTransactionReason): Promise<void> {
778 await this.stop(reason)
779 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
780 await sleep(this.stationInfo!.resetTime!)
781 this.initialize()
782 this.start()
783 }
784
785 public saveOcppConfiguration (): void {
786 if (this.stationInfo?.ocppPersistentConfiguration === true) {
787 this.saveConfiguration()
788 }
789 }
790
791 public bufferMessage (message: string): void {
792 this.messageBuffer.add(message)
793 this.setIntervalFlushMessageBuffer()
794 }
795
796 public openWSConnection (
797 options?: WsOptions,
798 params?: { closeOpened?: boolean, terminateOpened?: boolean }
799 ): void {
800 options = {
801 handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
802 ...this.stationInfo?.wsOptions,
803 ...options
804 }
805 params = { ...{ closeOpened: false, terminateOpened: false }, ...params }
806 if (!checkChargingStation(this, this.logPrefix())) {
807 return
808 }
809 if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) {
810 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
811 }
812 if (params.closeOpened === true) {
813 this.closeWSConnection()
814 }
815 if (params.terminateOpened === true) {
816 this.terminateWSConnection()
817 }
818
819 if (this.isWebSocketConnectionOpened()) {
820 logger.warn(
821 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
822 )
823 return
824 }
825
826 logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
827
828 this.wsConnection = new WebSocket(
829 this.wsConnectionUrl,
830 `ocpp${this.stationInfo?.ocppVersion}`,
831 options
832 )
833
834 // Handle WebSocket message
835 this.wsConnection.on('message', data => {
836 this.onMessage(data).catch(Constants.EMPTY_FUNCTION)
837 })
838 // Handle WebSocket error
839 this.wsConnection.on('error', this.onError.bind(this))
840 // Handle WebSocket close
841 this.wsConnection.on('close', this.onClose.bind(this))
842 // Handle WebSocket open
843 this.wsConnection.on('open', () => {
844 this.onOpen().catch((error: unknown) =>
845 logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
846 )
847 })
848 // Handle WebSocket ping
849 this.wsConnection.on('ping', this.onPing.bind(this))
850 // Handle WebSocket pong
851 this.wsConnection.on('pong', this.onPong.bind(this))
852 }
853
854 public closeWSConnection (): void {
855 if (this.isWebSocketConnectionOpened()) {
856 this.wsConnection?.close()
857 this.wsConnection = null
858 }
859 }
860
861 public getAutomaticTransactionGeneratorConfiguration ():
862 | AutomaticTransactionGeneratorConfiguration
863 | undefined {
864 if (this.automaticTransactionGeneratorConfiguration == null) {
865 let automaticTransactionGeneratorConfiguration:
866 | AutomaticTransactionGeneratorConfiguration
867 | undefined
868 const stationTemplate = this.getTemplateFromFile()
869 const stationConfiguration = this.getConfigurationFromFile()
870 if (
871 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
872 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
873 stationConfiguration?.automaticTransactionGenerator != null
874 ) {
875 automaticTransactionGeneratorConfiguration =
876 stationConfiguration.automaticTransactionGenerator
877 } else {
878 automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
879 }
880 this.automaticTransactionGeneratorConfiguration = {
881 ...Constants.DEFAULT_ATG_CONFIGURATION,
882 ...automaticTransactionGeneratorConfiguration
883 }
884 }
885 return this.automaticTransactionGeneratorConfiguration
886 }
887
888 public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
889 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
890 }
891
892 public startAutomaticTransactionGenerator (
893 connectorIds?: number[],
894 stopAbsoluteDuration?: boolean
895 ): void {
896 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
897 if (isNotEmptyArray(connectorIds)) {
898 for (const connectorId of connectorIds) {
899 this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration)
900 }
901 } else {
902 this.automaticTransactionGenerator?.start(stopAbsoluteDuration)
903 }
904 this.saveAutomaticTransactionGeneratorConfiguration()
905 this.emit(ChargingStationEvents.updated)
906 }
907
908 public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
909 if (isNotEmptyArray(connectorIds)) {
910 for (const connectorId of connectorIds) {
911 this.automaticTransactionGenerator?.stopConnector(connectorId)
912 }
913 } else {
914 this.automaticTransactionGenerator?.stop()
915 }
916 this.saveAutomaticTransactionGeneratorConfiguration()
917 this.emit(ChargingStationEvents.updated)
918 }
919
920 public async stopTransactionOnConnector (
921 connectorId: number,
922 reason?: StopTransactionReason
923 ): Promise<StopTransactionResponse> {
924 const transactionId = this.getConnectorStatus(connectorId)?.transactionId
925 if (
926 this.stationInfo?.beginEndMeterValues === true &&
927 this.stationInfo.ocppStrictCompliance === true &&
928 this.stationInfo.outOfOrderEndMeterValues === false
929 ) {
930 const transactionEndMeterValue = buildTransactionEndMeterValue(
931 this,
932 connectorId,
933 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
934 )
935 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
936 this,
937 RequestCommand.METER_VALUES,
938 {
939 connectorId,
940 transactionId,
941 meterValue: [transactionEndMeterValue]
942 }
943 )
944 }
945 return await this.ocppRequestService.requestHandler<
946 StopTransactionRequest,
947 StopTransactionResponse
948 >(this, RequestCommand.STOP_TRANSACTION, {
949 transactionId,
950 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
951 ...(reason != null && { reason })
952 })
953 }
954
955 public getReserveConnectorZeroSupported (): boolean {
956 return convertToBoolean(
957 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
958 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
959 )
960 }
961
962 public async addReservation (reservation: Reservation): Promise<void> {
963 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
964 if (reservationFound != null) {
965 await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
966 }
967 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
968 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
969 await sendAndSetConnectorStatus(
970 this,
971 reservation.connectorId,
972 ConnectorStatusEnum.Reserved,
973 undefined,
974 { send: reservation.connectorId !== 0 }
975 )
976 }
977
978 public async removeReservation (
979 reservation: Reservation,
980 reason: ReservationTerminationReason
981 ): Promise<void> {
982 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
983 const connector = this.getConnectorStatus(reservation.connectorId)!
984 switch (reason) {
985 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
986 case ReservationTerminationReason.TRANSACTION_STARTED:
987 delete connector.reservation
988 break
989 case ReservationTerminationReason.RESERVATION_CANCELED:
990 case ReservationTerminationReason.REPLACE_EXISTING:
991 case ReservationTerminationReason.EXPIRED:
992 await sendAndSetConnectorStatus(
993 this,
994 reservation.connectorId,
995 ConnectorStatusEnum.Available,
996 undefined,
997 { send: reservation.connectorId !== 0 }
998 )
999 delete connector.reservation
1000 break
1001 default:
1002 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1003 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
1004 }
1005 }
1006
1007 public getReservationBy (
1008 filterKey: ReservationKey,
1009 value: number | string
1010 ): Reservation | undefined {
1011 if (this.hasEvses) {
1012 for (const evseStatus of this.evses.values()) {
1013 for (const connectorStatus of evseStatus.connectors.values()) {
1014 if (connectorStatus.reservation?.[filterKey] === value) {
1015 return connectorStatus.reservation
1016 }
1017 }
1018 }
1019 } else {
1020 for (const connectorStatus of this.connectors.values()) {
1021 if (connectorStatus.reservation?.[filterKey] === value) {
1022 return connectorStatus.reservation
1023 }
1024 }
1025 }
1026 }
1027
1028 public isConnectorReservable (
1029 reservationId: number,
1030 idTag?: string,
1031 connectorId?: number
1032 ): boolean {
1033 const reservation = this.getReservationBy('reservationId', reservationId)
1034 const reservationExists = reservation != null && !hasReservationExpired(reservation)
1035 if (arguments.length === 1) {
1036 return !reservationExists
1037 } else if (arguments.length > 1) {
1038 const userReservation = idTag != null ? this.getReservationBy('idTag', idTag) : undefined
1039 const userReservationExists =
1040 userReservation != null && !hasReservationExpired(userReservation)
1041 const notConnectorZero = connectorId == null ? true : connectorId > 0
1042 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
1043 return (
1044 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
1045 )
1046 }
1047 return false
1048 }
1049
1050 private setIntervalFlushMessageBuffer (): void {
1051 if (this.flushMessageBufferSetInterval == null) {
1052 this.flushMessageBufferSetInterval = setInterval(() => {
1053 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1054 this.flushMessageBuffer()
1055 }
1056 if (this.messageBuffer.size === 0) {
1057 this.clearIntervalFlushMessageBuffer()
1058 }
1059 }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
1060 }
1061 }
1062
1063 private clearIntervalFlushMessageBuffer (): void {
1064 if (this.flushMessageBufferSetInterval != null) {
1065 clearInterval(this.flushMessageBufferSetInterval)
1066 delete this.flushMessageBufferSetInterval
1067 }
1068 }
1069
1070 private getNumberOfReservableConnectors (): number {
1071 let numberOfReservableConnectors = 0
1072 if (this.hasEvses) {
1073 for (const evseStatus of this.evses.values()) {
1074 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
1075 }
1076 } else {
1077 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
1078 }
1079 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
1080 }
1081
1082 private getNumberOfReservationsOnConnectorZero (): number {
1083 if (
1084 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
1085 (!this.hasEvses && this.connectors.get(0)?.reservation != null)
1086 ) {
1087 return 1
1088 }
1089 return 0
1090 }
1091
1092 private flushMessageBuffer (): void {
1093 if (this.messageBuffer.size > 0) {
1094 for (const message of this.messageBuffer.values()) {
1095 let beginId: string | undefined
1096 let commandName: RequestCommand | undefined
1097 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse
1098 const isRequest = messageType === MessageType.CALL_MESSAGE
1099 if (isRequest) {
1100 [, , commandName] = JSON.parse(message) as OutgoingRequest
1101 beginId = PerformanceStatistics.beginMeasure(commandName)
1102 }
1103 this.wsConnection?.send(message, (error?: Error) => {
1104 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1105 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
1106 if (error == null) {
1107 logger.debug(
1108 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1109 messageType
1110 )} OCPP message sent '${JSON.stringify(message)}'`
1111 )
1112 this.messageBuffer.delete(message)
1113 } else {
1114 logger.debug(
1115 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1116 messageType
1117 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1118 error
1119 )
1120 }
1121 })
1122 }
1123 }
1124 }
1125
1126 private getTemplateFromFile (): ChargingStationTemplate | undefined {
1127 let template: ChargingStationTemplate | undefined
1128 try {
1129 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1130 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
1131 } else {
1132 const measureId = `${FileType.ChargingStationTemplate} read`
1133 const beginId = PerformanceStatistics.beginMeasure(measureId)
1134 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
1135 PerformanceStatistics.endMeasure(measureId, beginId)
1136 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1137 .update(JSON.stringify(template))
1138 .digest('hex')
1139 this.sharedLRUCache.setChargingStationTemplate(template)
1140 this.templateFileHash = template.templateHash
1141 }
1142 } catch (error) {
1143 handleFileException(
1144 this.templateFile,
1145 FileType.ChargingStationTemplate,
1146 error as NodeJS.ErrnoException,
1147 this.logPrefix()
1148 )
1149 }
1150 return template
1151 }
1152
1153 private getStationInfoFromTemplate (): ChargingStationInfo {
1154 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1155 const stationTemplate = this.getTemplateFromFile()!
1156 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1157 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation)
1158 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
1159 if (stationTemplate.Connectors != null) {
1160 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1161 }
1162 const stationInfo = stationTemplateToStationInfo(stationTemplate)
1163 stationInfo.hashId = getHashId(this.index, stationTemplate)
1164 stationInfo.templateIndex = this.index
1165 stationInfo.templateName = buildTemplateName(this.templateFile)
1166 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
1167 createSerialNumber(stationTemplate, stationInfo)
1168 stationInfo.voltageOut = this.getVoltageOut(stationInfo)
1169 if (isNotEmptyArray(stationTemplate.power)) {
1170 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
1171 stationInfo.maximumPower =
1172 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1173 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1174 : stationTemplate.power[powerArrayRandomIndex]
1175 } else {
1176 stationInfo.maximumPower =
1177 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1178 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1179 stationTemplate.power! * 1000
1180 : stationTemplate.power
1181 }
1182 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
1183 if (
1184 isNotEmptyString(stationInfo.firmwareVersionPattern) &&
1185 isNotEmptyString(stationInfo.firmwareVersion) &&
1186 !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
1187 ) {
1188 logger.warn(
1189 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1190 this.templateFile
1191 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1192 )
1193 }
1194 if (stationTemplate.resetTime != null) {
1195 stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
1196 }
1197 return stationInfo
1198 }
1199
1200 private getStationInfoFromFile (
1201 stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO
1202 .stationInfoPersistentConfiguration
1203 ): ChargingStationInfo | undefined {
1204 let stationInfo: ChargingStationInfo | undefined
1205 if (stationInfoPersistentConfiguration === true) {
1206 stationInfo = this.getConfigurationFromFile()?.stationInfo
1207 if (stationInfo != null) {
1208 delete stationInfo.infoHash
1209 delete (stationInfo as ChargingStationTemplate).numberOfConnectors
1210 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1211 if (stationInfo.templateIndex == null) {
1212 stationInfo.templateIndex = this.index
1213 }
1214 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1215 if (stationInfo.templateName == null) {
1216 stationInfo.templateName = buildTemplateName(this.templateFile)
1217 }
1218 }
1219 }
1220 return stationInfo
1221 }
1222
1223 private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo {
1224 const stationInfoFromTemplate = this.getStationInfoFromTemplate()
1225 options?.persistentConfiguration != null &&
1226 (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration)
1227 const stationInfoFromFile = this.getStationInfoFromFile(
1228 stationInfoFromTemplate.stationInfoPersistentConfiguration
1229 )
1230 let stationInfo: ChargingStationInfo
1231 // Priority:
1232 // 1. charging station info from template
1233 // 2. charging station info from configuration file
1234 if (
1235 stationInfoFromFile != null &&
1236 stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
1237 ) {
1238 stationInfo = stationInfoFromFile
1239 } else {
1240 stationInfo = stationInfoFromTemplate
1241 stationInfoFromFile != null &&
1242 propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile, stationInfo)
1243 }
1244 return setChargingStationOptions(
1245 mergeDeepRight(Constants.DEFAULT_STATION_INFO, stationInfo),
1246 options
1247 )
1248 }
1249
1250 private saveStationInfo (): void {
1251 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1252 this.saveConfiguration()
1253 }
1254 }
1255
1256 private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
1257 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1258 logger.error(`${this.logPrefix()} ${errorMsg}`)
1259 throw new BaseError(errorMsg)
1260 }
1261
1262 private initialize (options?: ChargingStationOptions): void {
1263 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1264 const stationTemplate = this.getTemplateFromFile()!
1265 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1266 this.configurationFile = join(
1267 dirname(this.templateFile.replace('station-templates', 'configurations')),
1268 `${getHashId(this.index, stationTemplate)}.json`
1269 )
1270 const stationConfiguration = this.getConfigurationFromFile()
1271 if (
1272 stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash &&
1273 (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
1274 ) {
1275 checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
1276 this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
1277 } else {
1278 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
1279 }
1280 this.stationInfo = this.getStationInfo(options)
1281 if (
1282 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1283 isNotEmptyString(this.stationInfo.firmwareVersionPattern) &&
1284 isNotEmptyString(this.stationInfo.firmwareVersion)
1285 ) {
1286 const patternGroup =
1287 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1288 this.stationInfo.firmwareVersion.split('.').length
1289 const match = new RegExp(this.stationInfo.firmwareVersionPattern)
1290 .exec(this.stationInfo.firmwareVersion)
1291 ?.slice(1, patternGroup + 1)
1292 if (match != null) {
1293 const patchLevelIndex = match.length - 1
1294 match[patchLevelIndex] = (
1295 convertToInt(match[patchLevelIndex]) +
1296 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1297 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1298 ).toString()
1299 this.stationInfo.firmwareVersion = match.join('.')
1300 }
1301 }
1302 this.saveStationInfo()
1303 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
1304 if (this.stationInfo.enableStatistics === true) {
1305 this.performanceStatistics = PerformanceStatistics.getInstance(
1306 this.stationInfo.hashId,
1307 this.stationInfo.chargingStationId,
1308 this.configuredSupervisionUrl
1309 )
1310 }
1311 const bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
1312 if (bootNotificationRequest == null) {
1313 const errorMsg = 'Error while creating boot notification request'
1314 logger.error(`${this.logPrefix()} ${errorMsg}`)
1315 throw new BaseError(errorMsg)
1316 }
1317 this.bootNotificationRequest = bootNotificationRequest
1318 this.powerDivider = this.getPowerDivider()
1319 // OCPP configuration
1320 this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration)
1321 this.initializeOcppConfiguration()
1322 this.initializeOcppServices()
1323 if (this.stationInfo.autoRegister === true) {
1324 this.bootNotificationResponse = {
1325 currentTime: new Date(),
1326 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
1327 status: RegistrationStatusEnumType.ACCEPTED
1328 }
1329 }
1330 }
1331
1332 private initializeOcppServices (): void {
1333 const ocppVersion = this.stationInfo?.ocppVersion
1334 switch (ocppVersion) {
1335 case OCPPVersion.VERSION_16:
1336 this.ocppIncomingRequestService =
1337 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
1338 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1339 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1340 )
1341 break
1342 case OCPPVersion.VERSION_20:
1343 case OCPPVersion.VERSION_201:
1344 this.ocppIncomingRequestService =
1345 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
1346 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1347 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1348 )
1349 break
1350 default:
1351 this.handleUnsupportedVersion(ocppVersion)
1352 break
1353 }
1354 }
1355
1356 private initializeOcppConfiguration (): void {
1357 if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
1358 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
1359 }
1360 if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
1361 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false })
1362 }
1363 if (
1364 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
1365 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1366 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null
1367 ) {
1368 addConfigurationKey(
1369 this,
1370 this.stationInfo.supervisionUrlOcppKey,
1371 this.configuredSupervisionUrl.href,
1372 { reboot: true }
1373 )
1374 } else if (
1375 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
1376 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1377 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
1378 ) {
1379 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false })
1380 }
1381 if (
1382 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1383 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null
1384 ) {
1385 addConfigurationKey(
1386 this,
1387 this.stationInfo.amperageLimitationOcppKey,
1388 // prettier-ignore
1389 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1390 (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
1391 )
1392 }
1393 if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
1394 addConfigurationKey(
1395 this,
1396 StandardParametersKey.SupportedFeatureProfiles,
1397 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1398 )
1399 }
1400 addConfigurationKey(
1401 this,
1402 StandardParametersKey.NumberOfConnectors,
1403 this.getNumberOfConnectors().toString(),
1404 { readonly: true },
1405 { overwrite: true }
1406 )
1407 if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
1408 addConfigurationKey(
1409 this,
1410 StandardParametersKey.MeterValuesSampledData,
1411 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1412 )
1413 }
1414 if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
1415 const connectorsPhaseRotation: string[] = []
1416 if (this.hasEvses) {
1417 for (const evseStatus of this.evses.values()) {
1418 for (const connectorId of evseStatus.connectors.keys()) {
1419 connectorsPhaseRotation.push(
1420 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1421 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1422 )
1423 }
1424 }
1425 } else {
1426 for (const connectorId of this.connectors.keys()) {
1427 connectorsPhaseRotation.push(
1428 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1429 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1430 )
1431 }
1432 }
1433 addConfigurationKey(
1434 this,
1435 StandardParametersKey.ConnectorPhaseRotation,
1436 connectorsPhaseRotation.toString()
1437 )
1438 }
1439 if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
1440 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
1441 }
1442 if (
1443 getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
1444 hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
1445 ) {
1446 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
1447 }
1448 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
1449 addConfigurationKey(
1450 this,
1451 StandardParametersKey.ConnectionTimeOut,
1452 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1453 )
1454 }
1455 this.saveOcppConfiguration()
1456 }
1457
1458 private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
1459 if (configuration.connectorsStatus != null && configuration.evsesStatus == null) {
1460 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1461 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1462 }
1463 } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
1464 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1465 const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
1466 delete evseStatus.connectorsStatus
1467 this.evses.set(evseId, {
1468 ...(evseStatus as EvseStatus),
1469 connectors: new Map<number, ConnectorStatus>(
1470 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1471 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
1472 connectorId,
1473 connectorStatus
1474 ])
1475 )
1476 })
1477 }
1478 } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) {
1479 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1480 logger.error(`${this.logPrefix()} ${errorMsg}`)
1481 throw new BaseError(errorMsg)
1482 } else {
1483 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
1484 logger.error(`${this.logPrefix()} ${errorMsg}`)
1485 throw new BaseError(errorMsg)
1486 }
1487 }
1488
1489 private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1490 if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
1491 this.initializeConnectorsFromTemplate(stationTemplate)
1492 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
1493 this.initializeEvsesFromTemplate(stationTemplate)
1494 } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) {
1495 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
1496 logger.error(`${this.logPrefix()} ${errorMsg}`)
1497 throw new BaseError(errorMsg)
1498 } else {
1499 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
1500 logger.error(`${this.logPrefix()} ${errorMsg}`)
1501 throw new BaseError(errorMsg)
1502 }
1503 }
1504
1505 private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
1506 if (stationTemplate.Connectors == null && this.connectors.size === 0) {
1507 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1508 logger.error(`${this.logPrefix()} ${errorMsg}`)
1509 throw new BaseError(errorMsg)
1510 }
1511 if (stationTemplate.Connectors?.[0] == null) {
1512 logger.warn(
1513 `${this.logPrefix()} Charging station information from template ${
1514 this.templateFile
1515 } with no connector id 0 configuration`
1516 )
1517 }
1518 if (stationTemplate.Connectors != null) {
1519 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1520 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1521 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1522 .update(
1523 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1524 )
1525 .digest('hex')
1526 const connectorsConfigChanged =
1527 this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
1528 if (this.connectors.size === 0 || connectorsConfigChanged) {
1529 connectorsConfigChanged && this.connectors.clear()
1530 this.connectorsConfigurationHash = connectorsConfigHash
1531 if (templateMaxConnectors > 0) {
1532 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1533 if (
1534 connectorId === 0 &&
1535 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1536 (stationTemplate.Connectors[connectorId] == null ||
1537 !this.getUseConnectorId0(stationTemplate))
1538 ) {
1539 continue
1540 }
1541 const templateConnectorId =
1542 connectorId > 0 && stationTemplate.randomConnectors === true
1543 ? randomInt(1, templateMaxAvailableConnectors)
1544 : connectorId
1545 const connectorStatus = stationTemplate.Connectors[templateConnectorId]
1546 checkStationInfoConnectorStatus(
1547 templateConnectorId,
1548 connectorStatus,
1549 this.logPrefix(),
1550 this.templateFile
1551 )
1552 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1553 }
1554 initializeConnectorsMapStatus(this.connectors, this.logPrefix())
1555 this.saveConnectorsStatus()
1556 } else {
1557 logger.warn(
1558 `${this.logPrefix()} Charging station information from template ${
1559 this.templateFile
1560 } with no connectors configuration defined, cannot create connectors`
1561 )
1562 }
1563 }
1564 } else {
1565 logger.warn(
1566 `${this.logPrefix()} Charging station information from template ${
1567 this.templateFile
1568 } with no connectors configuration defined, using already defined connectors`
1569 )
1570 }
1571 }
1572
1573 private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1574 if (stationTemplate.Evses == null && this.evses.size === 0) {
1575 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1576 logger.error(`${this.logPrefix()} ${errorMsg}`)
1577 throw new BaseError(errorMsg)
1578 }
1579 if (stationTemplate.Evses?.[0] == null) {
1580 logger.warn(
1581 `${this.logPrefix()} Charging station information from template ${
1582 this.templateFile
1583 } with no evse id 0 configuration`
1584 )
1585 }
1586 if (stationTemplate.Evses?.[0]?.Connectors[0] == null) {
1587 logger.warn(
1588 `${this.logPrefix()} Charging station information from template ${
1589 this.templateFile
1590 } with evse id 0 with no connector id 0 configuration`
1591 )
1592 }
1593 if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) {
1594 logger.warn(
1595 `${this.logPrefix()} Charging station information from template ${
1596 this.templateFile
1597 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1598 )
1599 }
1600 if (stationTemplate.Evses != null) {
1601 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1602 .update(JSON.stringify(stationTemplate.Evses))
1603 .digest('hex')
1604 const evsesConfigChanged =
1605 this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
1606 if (this.evses.size === 0 || evsesConfigChanged) {
1607 evsesConfigChanged && this.evses.clear()
1608 this.evsesConfigurationHash = evsesConfigHash
1609 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses)
1610 if (templateMaxEvses > 0) {
1611 for (const evseKey in stationTemplate.Evses) {
1612 const evseId = convertToInt(evseKey)
1613 this.evses.set(evseId, {
1614 connectors: buildConnectorsMap(
1615 stationTemplate.Evses[evseKey].Connectors,
1616 this.logPrefix(),
1617 this.templateFile
1618 ),
1619 availability: AvailabilityType.Operative
1620 })
1621 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1622 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
1623 }
1624 this.saveEvsesStatus()
1625 } else {
1626 logger.warn(
1627 `${this.logPrefix()} Charging station information from template ${
1628 this.templateFile
1629 } with no evses configuration defined, cannot create evses`
1630 )
1631 }
1632 }
1633 } else {
1634 logger.warn(
1635 `${this.logPrefix()} Charging station information from template ${
1636 this.templateFile
1637 } with no evses configuration defined, using already defined evses`
1638 )
1639 }
1640 }
1641
1642 private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
1643 let configuration: ChargingStationConfiguration | undefined
1644 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
1645 try {
1646 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1647 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1648 this.configurationFileHash
1649 )
1650 } else {
1651 const measureId = `${FileType.ChargingStationConfiguration} read`
1652 const beginId = PerformanceStatistics.beginMeasure(measureId)
1653 configuration = JSON.parse(
1654 readFileSync(this.configurationFile, 'utf8')
1655 ) as ChargingStationConfiguration
1656 PerformanceStatistics.endMeasure(measureId, beginId)
1657 this.sharedLRUCache.setChargingStationConfiguration(configuration)
1658 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1659 this.configurationFileHash = configuration.configurationHash!
1660 }
1661 } catch (error) {
1662 handleFileException(
1663 this.configurationFile,
1664 FileType.ChargingStationConfiguration,
1665 error as NodeJS.ErrnoException,
1666 this.logPrefix()
1667 )
1668 }
1669 }
1670 return configuration
1671 }
1672
1673 private saveAutomaticTransactionGeneratorConfiguration (): void {
1674 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
1675 this.saveConfiguration()
1676 }
1677 }
1678
1679 private saveConnectorsStatus (): void {
1680 this.saveConfiguration()
1681 }
1682
1683 private saveEvsesStatus (): void {
1684 this.saveConfiguration()
1685 }
1686
1687 private saveConfiguration (): void {
1688 if (isNotEmptyString(this.configurationFile)) {
1689 try {
1690 if (!existsSync(dirname(this.configurationFile))) {
1691 mkdirSync(dirname(this.configurationFile), { recursive: true })
1692 }
1693 const configurationFromFile = this.getConfigurationFromFile()
1694 let configurationData: ChargingStationConfiguration =
1695 configurationFromFile != null
1696 ? clone<ChargingStationConfiguration>(configurationFromFile)
1697 : {}
1698 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1699 configurationData.stationInfo = this.stationInfo
1700 } else {
1701 delete configurationData.stationInfo
1702 }
1703 if (
1704 this.stationInfo?.ocppPersistentConfiguration === true &&
1705 Array.isArray(this.ocppConfiguration?.configurationKey)
1706 ) {
1707 configurationData.configurationKey = this.ocppConfiguration.configurationKey
1708 } else {
1709 delete configurationData.configurationKey
1710 }
1711 configurationData = mergeDeepRight(
1712 configurationData,
1713 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1714 )
1715 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) {
1716 delete configurationData.automaticTransactionGenerator
1717 }
1718 if (this.connectors.size > 0) {
1719 configurationData.connectorsStatus = buildConnectorsStatus(this)
1720 } else {
1721 delete configurationData.connectorsStatus
1722 }
1723 if (this.evses.size > 0) {
1724 configurationData.evsesStatus = buildEvsesStatus(this)
1725 } else {
1726 delete configurationData.evsesStatus
1727 }
1728 delete configurationData.configurationHash
1729 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1730 .update(
1731 JSON.stringify({
1732 stationInfo: configurationData.stationInfo,
1733 configurationKey: configurationData.configurationKey,
1734 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1735 ...(this.connectors.size > 0 && {
1736 connectorsStatus: configurationData.connectorsStatus
1737 }),
1738 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus })
1739 } satisfies ChargingStationConfiguration)
1740 )
1741 .digest('hex')
1742 if (this.configurationFileHash !== configurationHash) {
1743 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1744 configurationData.configurationHash = configurationHash
1745 const measureId = `${FileType.ChargingStationConfiguration} write`
1746 const beginId = PerformanceStatistics.beginMeasure(measureId)
1747 writeFileSync(
1748 this.configurationFile,
1749 JSON.stringify(configurationData, undefined, 2),
1750 'utf8'
1751 )
1752 PerformanceStatistics.endMeasure(measureId, beginId)
1753 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
1754 this.sharedLRUCache.setChargingStationConfiguration(configurationData)
1755 this.configurationFileHash = configurationHash
1756 }).catch((error: unknown) => {
1757 handleFileException(
1758 this.configurationFile,
1759 FileType.ChargingStationConfiguration,
1760 error as NodeJS.ErrnoException,
1761 this.logPrefix()
1762 )
1763 })
1764 } else {
1765 logger.debug(
1766 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1767 this.configurationFile
1768 }`
1769 )
1770 }
1771 } catch (error) {
1772 handleFileException(
1773 this.configurationFile,
1774 FileType.ChargingStationConfiguration,
1775 error as NodeJS.ErrnoException,
1776 this.logPrefix()
1777 )
1778 }
1779 } else {
1780 logger.error(
1781 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1782 )
1783 }
1784 }
1785
1786 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
1787 return this.getTemplateFromFile()?.Configuration
1788 }
1789
1790 private getOcppConfigurationFromFile (
1791 ocppPersistentConfiguration?: boolean
1792 ): ChargingStationOcppConfiguration | undefined {
1793 const configurationKey = this.getConfigurationFromFile()?.configurationKey
1794 if (ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
1795 return { configurationKey }
1796 }
1797 return undefined
1798 }
1799
1800 private getOcppConfiguration (
1801 ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration
1802 ): ChargingStationOcppConfiguration | undefined {
1803 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1804 this.getOcppConfigurationFromFile(ocppPersistentConfiguration)
1805 if (ocppConfiguration == null) {
1806 ocppConfiguration = this.getOcppConfigurationFromTemplate()
1807 }
1808 return ocppConfiguration
1809 }
1810
1811 private async onOpen (): Promise<void> {
1812 if (this.isWebSocketConnectionOpened()) {
1813 this.emit(ChargingStationEvents.updated)
1814 logger.info(
1815 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded`
1816 )
1817 let registrationRetryCount = 0
1818 if (!this.isRegistered()) {
1819 // Send BootNotification
1820 do {
1821 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1822 BootNotificationRequest,
1823 BootNotificationResponse
1824 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1825 skipBufferingOnError: true
1826 })
1827 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1828 if (this.bootNotificationResponse?.currentTime != null) {
1829 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1830 this.bootNotificationResponse.currentTime = convertToDate(
1831 this.bootNotificationResponse.currentTime
1832 )!
1833 }
1834 if (!this.isRegistered()) {
1835 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
1836 await sleep(
1837 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1838 this.bootNotificationResponse?.interval != null
1839 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1840 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1841 )
1842 }
1843 } while (
1844 !this.isRegistered() &&
1845 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1846 (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
1847 this.stationInfo?.registrationMaxRetries === -1)
1848 )
1849 }
1850 if (this.isRegistered()) {
1851 this.emit(ChargingStationEvents.registered)
1852 if (this.inAcceptedState()) {
1853 this.emit(ChargingStationEvents.accepted)
1854 }
1855 } else {
1856 if (this.inRejectedState()) {
1857 this.emit(ChargingStationEvents.rejected)
1858 }
1859 logger.error(
1860 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1861 this.stationInfo?.registrationMaxRetries
1862 })`
1863 )
1864 }
1865 this.wsConnectionRetryCount = 0
1866 this.emit(ChargingStationEvents.updated)
1867 } else {
1868 logger.warn(
1869 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
1870 )
1871 }
1872 }
1873
1874 private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
1875 this.emit(ChargingStationEvents.disconnected)
1876 this.emit(ChargingStationEvents.updated)
1877 switch (code) {
1878 // Normal close
1879 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1880 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1881 logger.info(
1882 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1883 code
1884 )}' and reason '${reason.toString()}'`
1885 )
1886 this.wsConnectionRetryCount = 0
1887 break
1888 // Abnormal close
1889 default:
1890 logger.error(
1891 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1892 code
1893 )}' and reason '${reason.toString()}'`
1894 )
1895 this.started &&
1896 this.reconnect()
1897 .then(() => {
1898 this.emit(ChargingStationEvents.updated)
1899 })
1900 .catch((error: unknown) =>
1901 logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
1902 )
1903 break
1904 }
1905 }
1906
1907 private getCachedRequest (
1908 messageType: MessageType | undefined,
1909 messageId: string
1910 ): CachedRequest | undefined {
1911 const cachedRequest = this.requests.get(messageId)
1912 if (Array.isArray(cachedRequest)) {
1913 return cachedRequest
1914 }
1915 throw new OCPPError(
1916 ErrorType.PROTOCOL_ERROR,
1917 `Cached request for message id ${messageId} ${getMessageTypeString(
1918 messageType
1919 )} is not an array`,
1920 undefined,
1921 cachedRequest
1922 )
1923 }
1924
1925 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1926 const [messageType, messageId, commandName, commandPayload] = request
1927 if (this.stationInfo?.enableStatistics === true) {
1928 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
1929 }
1930 logger.debug(
1931 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1932 request
1933 )}`
1934 )
1935 // Process the message
1936 await this.ocppIncomingRequestService.incomingRequestHandler(
1937 this,
1938 messageId,
1939 commandName,
1940 commandPayload
1941 )
1942 this.emit(ChargingStationEvents.updated)
1943 }
1944
1945 private handleResponseMessage (response: Response): void {
1946 const [messageType, messageId, commandPayload] = response
1947 if (!this.requests.has(messageId)) {
1948 // Error
1949 throw new OCPPError(
1950 ErrorType.INTERNAL_ERROR,
1951 `Response for unknown message id ${messageId}`,
1952 undefined,
1953 commandPayload
1954 )
1955 }
1956 // Respond
1957 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1958 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1959 messageType,
1960 messageId
1961 )!
1962 logger.debug(
1963 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1964 response
1965 )}`
1966 )
1967 responseCallback(commandPayload, requestPayload)
1968 }
1969
1970 private handleErrorMessage (errorResponse: ErrorResponse): void {
1971 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1972 if (!this.requests.has(messageId)) {
1973 // Error
1974 throw new OCPPError(
1975 ErrorType.INTERNAL_ERROR,
1976 `Error response for unknown message id ${messageId}`,
1977 undefined,
1978 { errorType, errorMessage, errorDetails }
1979 )
1980 }
1981 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1982 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1983 logger.debug(
1984 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1985 errorResponse
1986 )}`
1987 )
1988 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
1989 }
1990
1991 private async onMessage (data: RawData): Promise<void> {
1992 let request: IncomingRequest | Response | ErrorResponse | undefined
1993 let messageType: MessageType | undefined
1994 let errorMsg: string
1995 try {
1996 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1997 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
1998 if (Array.isArray(request)) {
1999 [messageType] = request
2000 // Check the type of message
2001 switch (messageType) {
2002 // Incoming Message
2003 case MessageType.CALL_MESSAGE:
2004 await this.handleIncomingMessage(request as IncomingRequest)
2005 break
2006 // Response Message
2007 case MessageType.CALL_RESULT_MESSAGE:
2008 this.handleResponseMessage(request as Response)
2009 break
2010 // Error Message
2011 case MessageType.CALL_ERROR_MESSAGE:
2012 this.handleErrorMessage(request as ErrorResponse)
2013 break
2014 // Unknown Message
2015 default:
2016 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2017 errorMsg = `Wrong message type ${messageType}`
2018 logger.error(`${this.logPrefix()} ${errorMsg}`)
2019 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
2020 }
2021 } else {
2022 throw new OCPPError(
2023 ErrorType.PROTOCOL_ERROR,
2024 'Incoming message is not an array',
2025 undefined,
2026 {
2027 request
2028 }
2029 )
2030 }
2031 } catch (error) {
2032 if (!Array.isArray(request)) {
2033 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
2034 return
2035 }
2036 let commandName: IncomingRequestCommand | undefined
2037 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
2038 let errorCallback: ErrorCallback
2039 const [, messageId] = request
2040 switch (messageType) {
2041 case MessageType.CALL_MESSAGE:
2042 [, , commandName] = request as IncomingRequest
2043 // Send error
2044 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
2045 break
2046 case MessageType.CALL_RESULT_MESSAGE:
2047 case MessageType.CALL_ERROR_MESSAGE:
2048 if (this.requests.has(messageId)) {
2049 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2050 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
2051 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2052 errorCallback(error as OCPPError, false)
2053 } else {
2054 // Remove the request from the cache in case of error at response handling
2055 this.requests.delete(messageId)
2056 }
2057 break
2058 }
2059 if (!(error instanceof OCPPError)) {
2060 logger.warn(
2061 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2062 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2063 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2064 }' message '${data.toString()}' handling is not an OCPPError:`,
2065 error
2066 )
2067 }
2068 logger.error(
2069 `${this.logPrefix()} Incoming OCPP command '${
2070 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2071 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2072 }' message '${data.toString()}'${
2073 this.requests.has(messageId)
2074 ? ` matching cached request '${JSON.stringify(this.getCachedRequest(messageType, messageId))}'`
2075 : ''
2076 } processing error:`,
2077 error
2078 )
2079 }
2080 }
2081
2082 private onPing (): void {
2083 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2084 }
2085
2086 private onPong (): void {
2087 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2088 }
2089
2090 private onError (error: WSError): void {
2091 this.closeWSConnection()
2092 logger.error(`${this.logPrefix()} WebSocket error:`, error)
2093 }
2094
2095 private getEnergyActiveImportRegister (
2096 connectorStatus: ConnectorStatus | undefined,
2097 rounded = false
2098 ): number {
2099 if (this.stationInfo?.meteringPerTransaction === true) {
2100 return (
2101 (rounded
2102 ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
2103 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
2104 : undefined
2105 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2106 )
2107 }
2108 return (
2109 (rounded
2110 ? connectorStatus?.energyActiveImportRegisterValue != null
2111 ? Math.round(connectorStatus.energyActiveImportRegisterValue)
2112 : undefined
2113 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2114 )
2115 }
2116
2117 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2118 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2119 return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
2120 }
2121
2122 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
2123 if (this.hasEvses) {
2124 for (const [evseId, evseStatus] of this.evses) {
2125 if (evseId === 0) {
2126 continue
2127 }
2128 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2129 if (connectorStatus.transactionStarted === true) {
2130 await this.stopTransactionOnConnector(connectorId, reason)
2131 }
2132 }
2133 }
2134 } else {
2135 for (const connectorId of this.connectors.keys()) {
2136 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2137 await this.stopTransactionOnConnector(connectorId, reason)
2138 }
2139 }
2140 }
2141 }
2142
2143 // 0 for disabling
2144 private getConnectionTimeout (): number {
2145 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
2146 return convertToInt(
2147 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
2148 Constants.DEFAULT_CONNECTION_TIMEOUT
2149 )
2150 }
2151 return Constants.DEFAULT_CONNECTION_TIMEOUT
2152 }
2153
2154 private getPowerDivider (): number {
2155 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
2156 if (this.stationInfo?.powerSharedByConnectors === true) {
2157 powerDivider = this.getNumberOfRunningTransactions()
2158 }
2159 return powerDivider
2160 }
2161
2162 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2163 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2164 const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
2165 switch (this.getCurrentOutType(stationInfo)) {
2166 case CurrentType.AC:
2167 return ACElectricUtils.amperagePerPhaseFromPower(
2168 this.getNumberOfPhases(stationInfo),
2169 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2170 this.getVoltageOut(stationInfo)
2171 )
2172 case CurrentType.DC:
2173 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
2174 }
2175 }
2176
2177 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
2178 return (
2179 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2180 (stationInfo ?? this.stationInfo!).currentOutType ??
2181 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2182 Constants.DEFAULT_STATION_INFO.currentOutType!
2183 )
2184 }
2185
2186 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
2187 return (
2188 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2189 (stationInfo ?? this.stationInfo!).voltageOut ??
2190 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
2191 )
2192 }
2193
2194 private getAmperageLimitation (): number | undefined {
2195 if (
2196 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2197 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
2198 ) {
2199 return (
2200 convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
2201 getAmperageLimitationUnitDivider(this.stationInfo)
2202 )
2203 }
2204 }
2205
2206 private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
2207 if (this.stationInfo?.autoRegister === true) {
2208 await this.ocppRequestService.requestHandler<
2209 BootNotificationRequest,
2210 BootNotificationResponse
2211 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2212 skipBufferingOnError: true
2213 })
2214 }
2215 // Start WebSocket ping
2216 this.startWebSocketPing()
2217 // Start heartbeat
2218 this.startHeartbeat()
2219 // Initialize connectors status
2220 if (this.hasEvses) {
2221 for (const [evseId, evseStatus] of this.evses) {
2222 if (evseId > 0) {
2223 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2224 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus)
2225 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId)
2226 }
2227 }
2228 }
2229 } else {
2230 for (const connectorId of this.connectors.keys()) {
2231 if (connectorId > 0) {
2232 const connectorBootStatus = getBootConnectorStatus(
2233 this,
2234 connectorId,
2235 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2236 this.getConnectorStatus(connectorId)!
2237 )
2238 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus)
2239 }
2240 }
2241 }
2242 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2243 await this.ocppRequestService.requestHandler<
2244 FirmwareStatusNotificationRequest,
2245 FirmwareStatusNotificationResponse
2246 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2247 status: FirmwareStatus.Installed
2248 })
2249 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
2250 }
2251
2252 // Start the ATG
2253 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2254 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
2255 }
2256 this.flushMessageBuffer()
2257 }
2258
2259 private internalStopMessageSequence (): void {
2260 // Stop WebSocket ping
2261 this.stopWebSocketPing()
2262 // Stop heartbeat
2263 this.stopHeartbeat()
2264 // Stop the ATG
2265 if (this.automaticTransactionGenerator?.started === true) {
2266 this.stopAutomaticTransactionGenerator()
2267 }
2268 }
2269
2270 private async stopMessageSequence (
2271 reason?: StopTransactionReason,
2272 stopTransactions?: boolean
2273 ): Promise<void> {
2274 this.internalStopMessageSequence()
2275 // Stop ongoing transactions
2276 stopTransactions === true && (await this.stopRunningTransactions(reason))
2277 if (this.hasEvses) {
2278 for (const [evseId, evseStatus] of this.evses) {
2279 if (evseId > 0) {
2280 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2281 await sendAndSetConnectorStatus(
2282 this,
2283 connectorId,
2284 ConnectorStatusEnum.Unavailable,
2285 evseId
2286 )
2287 delete connectorStatus.status
2288 }
2289 }
2290 }
2291 } else {
2292 for (const connectorId of this.connectors.keys()) {
2293 if (connectorId > 0) {
2294 await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
2295 delete this.getConnectorStatus(connectorId)?.status
2296 }
2297 }
2298 }
2299 }
2300
2301 private startWebSocketPing (): void {
2302 const webSocketPingInterval =
2303 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
2304 ? convertToInt(
2305 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
2306 )
2307 : 0
2308 if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
2309 this.wsPingSetInterval = setInterval(() => {
2310 if (this.isWebSocketConnectionOpened()) {
2311 this.wsConnection?.ping()
2312 }
2313 }, secondsToMilliseconds(webSocketPingInterval))
2314 logger.info(
2315 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2316 webSocketPingInterval
2317 )}`
2318 )
2319 } else if (this.wsPingSetInterval != null) {
2320 logger.info(
2321 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2322 webSocketPingInterval
2323 )}`
2324 )
2325 } else {
2326 logger.error(
2327 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2328 )
2329 }
2330 }
2331
2332 private stopWebSocketPing (): void {
2333 if (this.wsPingSetInterval != null) {
2334 clearInterval(this.wsPingSetInterval)
2335 delete this.wsPingSetInterval
2336 }
2337 }
2338
2339 private getConfiguredSupervisionUrl (): URL {
2340 let configuredSupervisionUrl: string
2341 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
2342 if (isNotEmptyArray(supervisionUrls)) {
2343 let configuredSupervisionUrlIndex: number
2344 switch (Configuration.getSupervisionUrlDistribution()) {
2345 case SupervisionUrlDistribution.RANDOM:
2346 configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
2347 break
2348 case SupervisionUrlDistribution.ROUND_ROBIN:
2349 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2350 default:
2351 !Object.values(SupervisionUrlDistribution).includes(
2352 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2353 Configuration.getSupervisionUrlDistribution()!
2354 ) &&
2355 logger.warn(
2356 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2357 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2358 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2359 }'`
2360 )
2361 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
2362 break
2363 }
2364 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
2365 } else {
2366 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2367 configuredSupervisionUrl = supervisionUrls!
2368 }
2369 if (isNotEmptyString(configuredSupervisionUrl)) {
2370 return new URL(configuredSupervisionUrl)
2371 }
2372 const errorMsg = 'No supervision url(s) configured'
2373 logger.error(`${this.logPrefix()} ${errorMsg}`)
2374 throw new BaseError(errorMsg)
2375 }
2376
2377 private stopHeartbeat (): void {
2378 if (this.heartbeatSetInterval != null) {
2379 clearInterval(this.heartbeatSetInterval)
2380 delete this.heartbeatSetInterval
2381 }
2382 }
2383
2384 private terminateWSConnection (): void {
2385 if (this.isWebSocketConnectionOpened()) {
2386 this.wsConnection?.terminate()
2387 this.wsConnection = null
2388 }
2389 }
2390
2391 private async reconnect (): Promise<void> {
2392 if (
2393 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2394 this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
2395 this.stationInfo?.autoReconnectMaxRetries === -1
2396 ) {
2397 this.wsConnectionRetried = true
2398 ++this.wsConnectionRetryCount
2399 const reconnectDelay =
2400 this.stationInfo?.reconnectExponentialDelay === true
2401 ? exponentialDelay(this.wsConnectionRetryCount)
2402 : secondsToMilliseconds(this.getConnectionTimeout())
2403 const reconnectDelayWithdraw = 1000
2404 const reconnectTimeout =
2405 reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
2406 logger.error(
2407 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2408 reconnectDelay,
2409 2
2410 )}ms, timeout ${reconnectTimeout}ms`
2411 )
2412 await sleep(reconnectDelay)
2413 logger.error(
2414 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2415 )
2416 this.openWSConnection(
2417 {
2418 handshakeTimeout: reconnectTimeout
2419 },
2420 { closeOpened: true }
2421 )
2422 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
2423 logger.error(
2424 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2425 this.wsConnectionRetryCount
2426 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
2427 )
2428 }
2429 }
2430 }