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