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