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