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