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