refactor: spell fix in log messages
[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, 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 getRandomInteger,
92 getWebSocketCloseEventStatusString,
93 handleFileException,
94 isNotEmptyArray,
95 isNotEmptyString,
96 logger,
97 logPrefix,
98 min,
99 roundTo,
100 secureRandom,
101 sleep,
102 watchJsonFile
103 } from '../utils/index.js'
104 import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js'
105 import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel.js'
106 import {
107 addConfigurationKey,
108 deleteConfigurationKey,
109 getConfigurationKey,
110 setConfigurationKeyValue
111 } from './ConfigurationKeyUtils.js'
112 import {
113 buildConnectorsMap,
114 buildTemplateName,
115 checkChargingStation,
116 checkConfiguration,
117 checkConnectorsConfiguration,
118 checkStationInfoConnectorStatus,
119 checkTemplate,
120 createBootNotificationRequest,
121 createSerialNumber,
122 getAmperageLimitationUnitDivider,
123 getBootConnectorStatus,
124 getChargingStationConnectorChargingProfilesPowerLimit,
125 getChargingStationId,
126 getDefaultVoltageOut,
127 getHashId,
128 getIdTagsFile,
129 getMaxNumberOfEvses,
130 getNumberOfReservableConnectors,
131 getPhaseRotationValue,
132 hasFeatureProfile,
133 hasReservationExpired,
134 initializeConnectorsMapStatus,
135 propagateSerialNumber,
136 setChargingStationOptions,
137 stationTemplateToStationInfo,
138 warnTemplateKeysDeprecation
139 } from './Helpers.js'
140 import { IdTagsCache } from './IdTagsCache.js'
141 import {
142 buildMeterValue,
143 buildTransactionEndMeterValue,
144 getMessageTypeString,
145 OCPP16IncomingRequestService,
146 OCPP16RequestService,
147 OCPP16ResponseService,
148 OCPP20IncomingRequestService,
149 OCPP20RequestService,
150 OCPP20ResponseService,
151 type OCPPIncomingRequestService,
152 type OCPPRequestService,
153 sendAndSetConnectorStatus
154 } from './ocpp/index.js'
155 import { SharedLRUCache } from './SharedLRUCache.js'
156
157 export class ChargingStation extends EventEmitter {
158 public readonly index: number
159 public readonly templateFile: string
160 public stationInfo?: ChargingStationInfo
161 public started: boolean
162 public starting: boolean
163 public idTagsCache: IdTagsCache
164 public automaticTransactionGenerator?: AutomaticTransactionGenerator
165 public ocppConfiguration?: ChargingStationOcppConfiguration
166 public wsConnection: WebSocket | null
167 public readonly connectors: Map<number, ConnectorStatus>
168 public readonly evses: Map<number, EvseStatus>
169 public readonly requests: Map<string, CachedRequest>
170 public performanceStatistics?: PerformanceStatistics
171 public heartbeatSetInterval?: NodeJS.Timeout
172 public ocppRequestService!: OCPPRequestService
173 public bootNotificationRequest?: BootNotificationRequest
174 public bootNotificationResponse?: BootNotificationResponse
175 public powerDivider?: number
176 private stopping: boolean
177 private configurationFile!: string
178 private configurationFileHash!: string
179 private connectorsConfigurationHash!: string
180 private evsesConfigurationHash!: string
181 private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
182 private ocppIncomingRequestService!: OCPPIncomingRequestService
183 private readonly messageBuffer: Set<string>
184 private configuredSupervisionUrl!: URL
185 private wsConnectionRetried: boolean
186 private wsConnectionRetryCount: number
187 private templateFileWatcher?: FSWatcher
188 private templateFileHash!: string
189 private readonly sharedLRUCache: SharedLRUCache
190 private wsPingSetInterval?: NodeJS.Timeout
191 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
192 private flushMessageBufferSetInterval?: NodeJS.Timeout
193
194 constructor (index: number, templateFile: string, options?: ChargingStationOptions) {
195 super()
196 this.started = false
197 this.starting = false
198 this.stopping = false
199 this.wsConnection = null
200 this.wsConnectionRetried = false
201 this.wsConnectionRetryCount = 0
202 this.index = index
203 this.templateFile = templateFile
204 this.connectors = new Map<number, ConnectorStatus>()
205 this.evses = new Map<number, EvseStatus>()
206 this.requests = new Map<string, CachedRequest>()
207 this.messageBuffer = new Set<string>()
208 this.sharedLRUCache = SharedLRUCache.getInstance()
209 this.idTagsCache = IdTagsCache.getInstance()
210 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this)
211
212 this.on(ChargingStationEvents.added, () => {
213 parentPort?.postMessage(buildAddedMessage(this))
214 })
215 this.on(ChargingStationEvents.deleted, () => {
216 parentPort?.postMessage(buildDeletedMessage(this))
217 })
218 this.on(ChargingStationEvents.started, () => {
219 parentPort?.postMessage(buildStartedMessage(this))
220 })
221 this.on(ChargingStationEvents.stopped, () => {
222 parentPort?.postMessage(buildStoppedMessage(this))
223 })
224 this.on(ChargingStationEvents.updated, () => {
225 parentPort?.postMessage(buildUpdatedMessage(this))
226 })
227 this.on(ChargingStationEvents.accepted, () => {
228 this.startMessageSequence(
229 this.wsConnectionRetried
230 ? true
231 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
232 ).catch(error => {
233 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
234 })
235 this.wsConnectionRetried = false
236 })
237 this.on(ChargingStationEvents.rejected, () => {
238 this.wsConnectionRetried = false
239 })
240 this.on(ChargingStationEvents.disconnected, () => {
241 try {
242 this.internalStopMessageSequence()
243 } catch (error) {
244 logger.error(
245 `${this.logPrefix()} Error while stopping the internal message sequence:`,
246 error
247 )
248 }
249 })
250
251 this.initialize(options)
252
253 this.add()
254
255 if (this.stationInfo?.autoStart === true) {
256 this.start()
257 }
258 }
259
260 public get hasEvses (): boolean {
261 return this.connectors.size === 0 && this.evses.size > 0
262 }
263
264 public get wsConnectionUrl (): URL {
265 return new URL(
266 `${
267 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
268 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
269 isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
270 ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
271 : this.configuredSupervisionUrl.href
272 }/${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 stationTemplate = undefined
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 => {
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 => {
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 =>
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 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 stationInfo.firmwareUpgrade = mergeDeepRight(
1196 {
1197 versionUpgrade: {
1198 step: 1
1199 },
1200 reset: true
1201 },
1202 stationTemplate.firmwareUpgrade ?? {}
1203 )
1204 if (stationTemplate.resetTime != null) {
1205 stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
1206 }
1207 return stationInfo
1208 }
1209
1210 private getStationInfoFromFile (
1211 stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO
1212 .stationInfoPersistentConfiguration
1213 ): ChargingStationInfo | undefined {
1214 let stationInfo: ChargingStationInfo | undefined
1215 if (stationInfoPersistentConfiguration === true) {
1216 stationInfo = this.getConfigurationFromFile()?.stationInfo
1217 if (stationInfo != null) {
1218 delete stationInfo.infoHash
1219 delete (stationInfo as ChargingStationTemplate).numberOfConnectors
1220 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1221 if (stationInfo.templateIndex == null) {
1222 stationInfo.templateIndex = this.index
1223 }
1224 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1225 if (stationInfo.templateName == null) {
1226 stationInfo.templateName = buildTemplateName(this.templateFile)
1227 }
1228 }
1229 }
1230 return stationInfo
1231 }
1232
1233 private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo {
1234 const stationInfoFromTemplate = this.getStationInfoFromTemplate()
1235 options?.persistentConfiguration != null &&
1236 (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration)
1237 const stationInfoFromFile = this.getStationInfoFromFile(
1238 stationInfoFromTemplate.stationInfoPersistentConfiguration
1239 )
1240 // Priority:
1241 // 1. charging station info from template
1242 // 2. charging station info from configuration file
1243 if (
1244 stationInfoFromFile != null &&
1245 stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
1246 ) {
1247 return setChargingStationOptions(
1248 { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromFile },
1249 options
1250 )
1251 }
1252 stationInfoFromFile != null &&
1253 propagateSerialNumber(
1254 this.getTemplateFromFile(),
1255 stationInfoFromFile,
1256 stationInfoFromTemplate
1257 )
1258 return setChargingStationOptions(
1259 { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromTemplate },
1260 options
1261 )
1262 }
1263
1264 private saveStationInfo (): void {
1265 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1266 this.saveConfiguration()
1267 }
1268 }
1269
1270 private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
1271 const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
1272 logger.error(`${this.logPrefix()} ${errorMsg}`)
1273 throw new BaseError(errorMsg)
1274 }
1275
1276 private initialize (options?: ChargingStationOptions): void {
1277 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1278 const stationTemplate = this.getTemplateFromFile()!
1279 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1280 this.configurationFile = join(
1281 dirname(this.templateFile.replace('station-templates', 'configurations')),
1282 `${getHashId(this.index, stationTemplate)}.json`
1283 )
1284 const stationConfiguration = this.getConfigurationFromFile()
1285 if (
1286 stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash &&
1287 (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
1288 ) {
1289 checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
1290 this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
1291 } else {
1292 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
1293 }
1294 this.stationInfo = this.getStationInfo(options)
1295 if (
1296 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
1297 isNotEmptyString(this.stationInfo.firmwareVersionPattern) &&
1298 isNotEmptyString(this.stationInfo.firmwareVersion)
1299 ) {
1300 const patternGroup =
1301 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
1302 this.stationInfo.firmwareVersion.split('.').length
1303 const match = new RegExp(this.stationInfo.firmwareVersionPattern)
1304 .exec(this.stationInfo.firmwareVersion)
1305 ?.slice(1, patternGroup + 1)
1306 if (match != null) {
1307 const patchLevelIndex = match.length - 1
1308 match[patchLevelIndex] = (
1309 convertToInt(match[patchLevelIndex]) +
1310 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1311 this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
1312 ).toString()
1313 this.stationInfo.firmwareVersion = match.join('.')
1314 }
1315 }
1316 this.saveStationInfo()
1317 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
1318 if (this.stationInfo.enableStatistics === true) {
1319 this.performanceStatistics = PerformanceStatistics.getInstance(
1320 this.stationInfo.hashId,
1321 this.stationInfo.chargingStationId,
1322 this.configuredSupervisionUrl
1323 )
1324 }
1325 const bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
1326 if (bootNotificationRequest == null) {
1327 const errorMsg = 'Error while creating boot notification request'
1328 logger.error(`${this.logPrefix()} ${errorMsg}`)
1329 throw new BaseError(errorMsg)
1330 }
1331 this.bootNotificationRequest = bootNotificationRequest
1332 this.powerDivider = this.getPowerDivider()
1333 // OCPP configuration
1334 this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration)
1335 this.initializeOcppConfiguration()
1336 this.initializeOcppServices()
1337 if (this.stationInfo.autoRegister === true) {
1338 this.bootNotificationResponse = {
1339 currentTime: new Date(),
1340 interval: millisecondsToSeconds(this.getHeartbeatInterval()),
1341 status: RegistrationStatusEnumType.ACCEPTED
1342 }
1343 }
1344 }
1345
1346 private initializeOcppServices (): void {
1347 const ocppVersion = this.stationInfo?.ocppVersion
1348 switch (ocppVersion) {
1349 case OCPPVersion.VERSION_16:
1350 this.ocppIncomingRequestService =
1351 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
1352 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1353 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
1354 )
1355 break
1356 case OCPPVersion.VERSION_20:
1357 case OCPPVersion.VERSION_201:
1358 this.ocppIncomingRequestService =
1359 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
1360 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1361 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1362 )
1363 break
1364 default:
1365 this.handleUnsupportedVersion(ocppVersion)
1366 break
1367 }
1368 }
1369
1370 private initializeOcppConfiguration (): void {
1371 if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
1372 addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
1373 }
1374 if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
1375 addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false })
1376 }
1377 if (
1378 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
1379 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1380 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null
1381 ) {
1382 addConfigurationKey(
1383 this,
1384 this.stationInfo.supervisionUrlOcppKey,
1385 this.configuredSupervisionUrl.href,
1386 { reboot: true }
1387 )
1388 } else if (
1389 this.stationInfo?.supervisionUrlOcppConfiguration === false &&
1390 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
1391 getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
1392 ) {
1393 deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false })
1394 }
1395 if (
1396 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
1397 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null
1398 ) {
1399 addConfigurationKey(
1400 this,
1401 this.stationInfo.amperageLimitationOcppKey,
1402 // prettier-ignore
1403 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1404 (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
1405 )
1406 }
1407 if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
1408 addConfigurationKey(
1409 this,
1410 StandardParametersKey.SupportedFeatureProfiles,
1411 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1412 )
1413 }
1414 addConfigurationKey(
1415 this,
1416 StandardParametersKey.NumberOfConnectors,
1417 this.getNumberOfConnectors().toString(),
1418 { readonly: true },
1419 { overwrite: true }
1420 )
1421 if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
1422 addConfigurationKey(
1423 this,
1424 StandardParametersKey.MeterValuesSampledData,
1425 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1426 )
1427 }
1428 if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
1429 const connectorsPhaseRotation: string[] = []
1430 if (this.hasEvses) {
1431 for (const evseStatus of this.evses.values()) {
1432 for (const connectorId of evseStatus.connectors.keys()) {
1433 connectorsPhaseRotation.push(
1434 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1435 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1436 )
1437 }
1438 }
1439 } else {
1440 for (const connectorId of this.connectors.keys()) {
1441 connectorsPhaseRotation.push(
1442 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1443 getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
1444 )
1445 }
1446 }
1447 addConfigurationKey(
1448 this,
1449 StandardParametersKey.ConnectorPhaseRotation,
1450 connectorsPhaseRotation.toString()
1451 )
1452 }
1453 if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
1454 addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
1455 }
1456 if (
1457 getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
1458 hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
1459 ) {
1460 addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
1461 }
1462 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
1463 addConfigurationKey(
1464 this,
1465 StandardParametersKey.ConnectionTimeOut,
1466 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1467 )
1468 }
1469 this.saveOcppConfiguration()
1470 }
1471
1472 private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
1473 if (configuration.connectorsStatus != null && configuration.evsesStatus == null) {
1474 for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
1475 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1476 }
1477 } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
1478 for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
1479 const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
1480 delete evseStatus.connectorsStatus
1481 this.evses.set(evseId, {
1482 ...(evseStatus as EvseStatus),
1483 connectors: new Map<number, ConnectorStatus>(
1484 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1485 evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
1486 connectorId,
1487 connectorStatus
1488 ])
1489 )
1490 })
1491 }
1492 } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) {
1493 const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
1494 logger.error(`${this.logPrefix()} ${errorMsg}`)
1495 throw new BaseError(errorMsg)
1496 } else {
1497 const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
1498 logger.error(`${this.logPrefix()} ${errorMsg}`)
1499 throw new BaseError(errorMsg)
1500 }
1501 }
1502
1503 private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1504 if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
1505 this.initializeConnectorsFromTemplate(stationTemplate)
1506 } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
1507 this.initializeEvsesFromTemplate(stationTemplate)
1508 } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) {
1509 const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
1510 logger.error(`${this.logPrefix()} ${errorMsg}`)
1511 throw new BaseError(errorMsg)
1512 } else {
1513 const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
1514 logger.error(`${this.logPrefix()} ${errorMsg}`)
1515 throw new BaseError(errorMsg)
1516 }
1517 }
1518
1519 private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
1520 if (stationTemplate.Connectors == null && this.connectors.size === 0) {
1521 const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
1522 logger.error(`${this.logPrefix()} ${errorMsg}`)
1523 throw new BaseError(errorMsg)
1524 }
1525 if (stationTemplate.Connectors?.[0] == null) {
1526 logger.warn(
1527 `${this.logPrefix()} Charging station information from template ${
1528 this.templateFile
1529 } with no connector id 0 configuration`
1530 )
1531 }
1532 if (stationTemplate.Connectors != null) {
1533 const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
1534 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1535 const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1536 .update(
1537 `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
1538 )
1539 .digest('hex')
1540 const connectorsConfigChanged =
1541 this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
1542 if (this.connectors.size === 0 || connectorsConfigChanged) {
1543 connectorsConfigChanged && this.connectors.clear()
1544 this.connectorsConfigurationHash = connectorsConfigHash
1545 if (templateMaxConnectors > 0) {
1546 for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
1547 if (
1548 connectorId === 0 &&
1549 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1550 (stationTemplate.Connectors[connectorId] == null ||
1551 !this.getUseConnectorId0(stationTemplate))
1552 ) {
1553 continue
1554 }
1555 const templateConnectorId =
1556 connectorId > 0 && stationTemplate.randomConnectors === true
1557 ? getRandomInteger(templateMaxAvailableConnectors, 1)
1558 : connectorId
1559 const connectorStatus = stationTemplate.Connectors[templateConnectorId]
1560 checkStationInfoConnectorStatus(
1561 templateConnectorId,
1562 connectorStatus,
1563 this.logPrefix(),
1564 this.templateFile
1565 )
1566 this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
1567 }
1568 initializeConnectorsMapStatus(this.connectors, this.logPrefix())
1569 this.saveConnectorsStatus()
1570 } else {
1571 logger.warn(
1572 `${this.logPrefix()} Charging station information from template ${
1573 this.templateFile
1574 } with no connectors configuration defined, cannot create connectors`
1575 )
1576 }
1577 }
1578 } else {
1579 logger.warn(
1580 `${this.logPrefix()} Charging station information from template ${
1581 this.templateFile
1582 } with no connectors configuration defined, using already defined connectors`
1583 )
1584 }
1585 }
1586
1587 private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
1588 if (stationTemplate.Evses == null && this.evses.size === 0) {
1589 const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
1590 logger.error(`${this.logPrefix()} ${errorMsg}`)
1591 throw new BaseError(errorMsg)
1592 }
1593 if (stationTemplate.Evses?.[0] == null) {
1594 logger.warn(
1595 `${this.logPrefix()} Charging station information from template ${
1596 this.templateFile
1597 } with no evse id 0 configuration`
1598 )
1599 }
1600 if (stationTemplate.Evses?.[0]?.Connectors[0] == null) {
1601 logger.warn(
1602 `${this.logPrefix()} Charging station information from template ${
1603 this.templateFile
1604 } with evse id 0 with no connector id 0 configuration`
1605 )
1606 }
1607 if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) {
1608 logger.warn(
1609 `${this.logPrefix()} Charging station information from template ${
1610 this.templateFile
1611 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
1612 )
1613 }
1614 if (stationTemplate.Evses != null) {
1615 const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1616 .update(JSON.stringify(stationTemplate.Evses))
1617 .digest('hex')
1618 const evsesConfigChanged =
1619 this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
1620 if (this.evses.size === 0 || evsesConfigChanged) {
1621 evsesConfigChanged && this.evses.clear()
1622 this.evsesConfigurationHash = evsesConfigHash
1623 const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses)
1624 if (templateMaxEvses > 0) {
1625 for (const evseKey in stationTemplate.Evses) {
1626 const evseId = convertToInt(evseKey)
1627 this.evses.set(evseId, {
1628 connectors: buildConnectorsMap(
1629 stationTemplate.Evses[evseKey].Connectors,
1630 this.logPrefix(),
1631 this.templateFile
1632 ),
1633 availability: AvailabilityType.Operative
1634 })
1635 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1636 initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
1637 }
1638 this.saveEvsesStatus()
1639 } else {
1640 logger.warn(
1641 `${this.logPrefix()} Charging station information from template ${
1642 this.templateFile
1643 } with no evses configuration defined, cannot create evses`
1644 )
1645 }
1646 }
1647 } else {
1648 logger.warn(
1649 `${this.logPrefix()} Charging station information from template ${
1650 this.templateFile
1651 } with no evses configuration defined, using already defined evses`
1652 )
1653 }
1654 }
1655
1656 private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
1657 let configuration: ChargingStationConfiguration | undefined
1658 if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
1659 try {
1660 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1661 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1662 this.configurationFileHash
1663 )
1664 } else {
1665 const measureId = `${FileType.ChargingStationConfiguration} read`
1666 const beginId = PerformanceStatistics.beginMeasure(measureId)
1667 configuration = JSON.parse(
1668 readFileSync(this.configurationFile, 'utf8')
1669 ) as ChargingStationConfiguration
1670 PerformanceStatistics.endMeasure(measureId, beginId)
1671 this.sharedLRUCache.setChargingStationConfiguration(configuration)
1672 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1673 this.configurationFileHash = configuration.configurationHash!
1674 }
1675 } catch (error) {
1676 handleFileException(
1677 this.configurationFile,
1678 FileType.ChargingStationConfiguration,
1679 error as NodeJS.ErrnoException,
1680 this.logPrefix()
1681 )
1682 }
1683 }
1684 return configuration
1685 }
1686
1687 private saveAutomaticTransactionGeneratorConfiguration (): void {
1688 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
1689 this.saveConfiguration()
1690 }
1691 }
1692
1693 private saveConnectorsStatus (): void {
1694 this.saveConfiguration()
1695 }
1696
1697 private saveEvsesStatus (): void {
1698 this.saveConfiguration()
1699 }
1700
1701 private saveConfiguration (): void {
1702 if (isNotEmptyString(this.configurationFile)) {
1703 try {
1704 if (!existsSync(dirname(this.configurationFile))) {
1705 mkdirSync(dirname(this.configurationFile), { recursive: true })
1706 }
1707 const configurationFromFile = this.getConfigurationFromFile()
1708 let configurationData: ChargingStationConfiguration =
1709 configurationFromFile != null
1710 ? clone<ChargingStationConfiguration>(configurationFromFile)
1711 : {}
1712 if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
1713 configurationData.stationInfo = this.stationInfo
1714 } else {
1715 delete configurationData.stationInfo
1716 }
1717 if (
1718 this.stationInfo?.ocppPersistentConfiguration === true &&
1719 Array.isArray(this.ocppConfiguration?.configurationKey)
1720 ) {
1721 configurationData.configurationKey = this.ocppConfiguration.configurationKey
1722 } else {
1723 delete configurationData.configurationKey
1724 }
1725 configurationData = mergeDeepRight(
1726 configurationData,
1727 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1728 )
1729 if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) {
1730 delete configurationData.automaticTransactionGenerator
1731 }
1732 if (this.connectors.size > 0) {
1733 configurationData.connectorsStatus = buildConnectorsStatus(this)
1734 } else {
1735 delete configurationData.connectorsStatus
1736 }
1737 if (this.evses.size > 0) {
1738 configurationData.evsesStatus = buildEvsesStatus(this)
1739 } else {
1740 delete configurationData.evsesStatus
1741 }
1742 delete configurationData.configurationHash
1743 const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1744 .update(
1745 JSON.stringify({
1746 stationInfo: configurationData.stationInfo,
1747 configurationKey: configurationData.configurationKey,
1748 automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
1749 ...(this.connectors.size > 0 && {
1750 connectorsStatus: configurationData.connectorsStatus
1751 }),
1752 ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus })
1753 } satisfies ChargingStationConfiguration)
1754 )
1755 .digest('hex')
1756 if (this.configurationFileHash !== configurationHash) {
1757 AsyncLock.runExclusive(AsyncLockType.configuration, () => {
1758 configurationData.configurationHash = configurationHash
1759 const measureId = `${FileType.ChargingStationConfiguration} write`
1760 const beginId = PerformanceStatistics.beginMeasure(measureId)
1761 writeFileSync(
1762 this.configurationFile,
1763 JSON.stringify(configurationData, undefined, 2),
1764 'utf8'
1765 )
1766 PerformanceStatistics.endMeasure(measureId, beginId)
1767 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
1768 this.sharedLRUCache.setChargingStationConfiguration(configurationData)
1769 this.configurationFileHash = configurationHash
1770 }).catch(error => {
1771 handleFileException(
1772 this.configurationFile,
1773 FileType.ChargingStationConfiguration,
1774 error as NodeJS.ErrnoException,
1775 this.logPrefix()
1776 )
1777 })
1778 } else {
1779 logger.debug(
1780 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1781 this.configurationFile
1782 }`
1783 )
1784 }
1785 } catch (error) {
1786 handleFileException(
1787 this.configurationFile,
1788 FileType.ChargingStationConfiguration,
1789 error as NodeJS.ErrnoException,
1790 this.logPrefix()
1791 )
1792 }
1793 } else {
1794 logger.error(
1795 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1796 )
1797 }
1798 }
1799
1800 private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
1801 return this.getTemplateFromFile()?.Configuration
1802 }
1803
1804 private getOcppConfigurationFromFile (
1805 ocppPersistentConfiguration?: boolean
1806 ): ChargingStationOcppConfiguration | undefined {
1807 const configurationKey = this.getConfigurationFromFile()?.configurationKey
1808 if (ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
1809 return { configurationKey }
1810 }
1811 return undefined
1812 }
1813
1814 private getOcppConfiguration (
1815 ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration
1816 ): ChargingStationOcppConfiguration | undefined {
1817 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
1818 this.getOcppConfigurationFromFile(ocppPersistentConfiguration)
1819 if (ocppConfiguration == null) {
1820 ocppConfiguration = this.getOcppConfigurationFromTemplate()
1821 }
1822 return ocppConfiguration
1823 }
1824
1825 private async onOpen (): Promise<void> {
1826 if (this.isWebSocketConnectionOpened()) {
1827 this.emit(ChargingStationEvents.updated)
1828 logger.info(
1829 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded`
1830 )
1831 let registrationRetryCount = 0
1832 if (!this.isRegistered()) {
1833 // Send BootNotification
1834 do {
1835 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1836 BootNotificationRequest,
1837 BootNotificationResponse
1838 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1839 skipBufferingOnError: true
1840 })
1841 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1842 if (this.bootNotificationResponse?.currentTime != null) {
1843 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1844 this.bootNotificationResponse.currentTime = convertToDate(
1845 this.bootNotificationResponse.currentTime
1846 )!
1847 }
1848 if (!this.isRegistered()) {
1849 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
1850 await sleep(
1851 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1852 this.bootNotificationResponse?.interval != null
1853 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1854 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1855 )
1856 }
1857 } while (
1858 !this.isRegistered() &&
1859 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1860 (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
1861 this.stationInfo?.registrationMaxRetries === -1)
1862 )
1863 }
1864 if (this.isRegistered()) {
1865 this.emit(ChargingStationEvents.registered)
1866 if (this.inAcceptedState()) {
1867 this.emit(ChargingStationEvents.accepted)
1868 }
1869 } else {
1870 if (this.inRejectedState()) {
1871 this.emit(ChargingStationEvents.rejected)
1872 }
1873 logger.error(
1874 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1875 this.stationInfo?.registrationMaxRetries
1876 })`
1877 )
1878 }
1879 this.wsConnectionRetryCount = 0
1880 this.emit(ChargingStationEvents.updated)
1881 } else {
1882 logger.warn(
1883 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
1884 )
1885 }
1886 }
1887
1888 private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
1889 this.emit(ChargingStationEvents.disconnected)
1890 this.emit(ChargingStationEvents.updated)
1891 switch (code) {
1892 // Normal close
1893 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1894 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1895 logger.info(
1896 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1897 code
1898 )}' and reason '${reason.toString()}'`
1899 )
1900 this.wsConnectionRetryCount = 0
1901 break
1902 // Abnormal close
1903 default:
1904 logger.error(
1905 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1906 code
1907 )}' and reason '${reason.toString()}'`
1908 )
1909 this.started &&
1910 this.reconnect()
1911 .then(() => {
1912 this.emit(ChargingStationEvents.updated)
1913 })
1914 .catch(error => logger.error(`${this.logPrefix()} Error while reconnecting:`, error))
1915 break
1916 }
1917 }
1918
1919 private getCachedRequest (
1920 messageType: MessageType | undefined,
1921 messageId: string
1922 ): CachedRequest | undefined {
1923 const cachedRequest = this.requests.get(messageId)
1924 if (Array.isArray(cachedRequest)) {
1925 return cachedRequest
1926 }
1927 throw new OCPPError(
1928 ErrorType.PROTOCOL_ERROR,
1929 `Cached request for message id ${messageId} ${getMessageTypeString(
1930 messageType
1931 )} is not an array`,
1932 undefined,
1933 cachedRequest
1934 )
1935 }
1936
1937 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1938 const [messageType, messageId, commandName, commandPayload] = request
1939 if (this.stationInfo?.enableStatistics === true) {
1940 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
1941 }
1942 logger.debug(
1943 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1944 request
1945 )}`
1946 )
1947 // Process the message
1948 await this.ocppIncomingRequestService.incomingRequestHandler(
1949 this,
1950 messageId,
1951 commandName,
1952 commandPayload
1953 )
1954 this.emit(ChargingStationEvents.updated)
1955 }
1956
1957 private handleResponseMessage (response: Response): void {
1958 const [messageType, messageId, commandPayload] = response
1959 if (!this.requests.has(messageId)) {
1960 // Error
1961 throw new OCPPError(
1962 ErrorType.INTERNAL_ERROR,
1963 `Response for unknown message id ${messageId}`,
1964 undefined,
1965 commandPayload
1966 )
1967 }
1968 // Respond
1969 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1970 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1971 messageType,
1972 messageId
1973 )!
1974 logger.debug(
1975 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1976 response
1977 )}`
1978 )
1979 responseCallback(commandPayload, requestPayload)
1980 }
1981
1982 private handleErrorMessage (errorResponse: ErrorResponse): void {
1983 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1984 if (!this.requests.has(messageId)) {
1985 // Error
1986 throw new OCPPError(
1987 ErrorType.INTERNAL_ERROR,
1988 `Error response for unknown message id ${messageId}`,
1989 undefined,
1990 { errorType, errorMessage, errorDetails }
1991 )
1992 }
1993 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1994 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1995 logger.debug(
1996 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1997 errorResponse
1998 )}`
1999 )
2000 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
2001 }
2002
2003 private async onMessage (data: RawData): Promise<void> {
2004 let request: IncomingRequest | Response | ErrorResponse | undefined
2005 let messageType: MessageType | undefined
2006 let errorMsg: string
2007 try {
2008 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2009 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
2010 if (Array.isArray(request)) {
2011 [messageType] = request
2012 // Check the type of message
2013 switch (messageType) {
2014 // Incoming Message
2015 case MessageType.CALL_MESSAGE:
2016 await this.handleIncomingMessage(request as IncomingRequest)
2017 break
2018 // Response Message
2019 case MessageType.CALL_RESULT_MESSAGE:
2020 this.handleResponseMessage(request as Response)
2021 break
2022 // Error Message
2023 case MessageType.CALL_ERROR_MESSAGE:
2024 this.handleErrorMessage(request as ErrorResponse)
2025 break
2026 // Unknown Message
2027 default:
2028 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2029 errorMsg = `Wrong message type ${messageType}`
2030 logger.error(`${this.logPrefix()} ${errorMsg}`)
2031 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
2032 }
2033 } else {
2034 throw new OCPPError(
2035 ErrorType.PROTOCOL_ERROR,
2036 'Incoming message is not an array',
2037 undefined,
2038 {
2039 request
2040 }
2041 )
2042 }
2043 } catch (error) {
2044 if (!Array.isArray(request)) {
2045 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
2046 return
2047 }
2048 let commandName: IncomingRequestCommand | undefined
2049 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
2050 let errorCallback: ErrorCallback
2051 const [, messageId] = request
2052 switch (messageType) {
2053 case MessageType.CALL_MESSAGE:
2054 [, , commandName] = request as IncomingRequest
2055 // Send error
2056 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
2057 break
2058 case MessageType.CALL_RESULT_MESSAGE:
2059 case MessageType.CALL_ERROR_MESSAGE:
2060 if (this.requests.has(messageId)) {
2061 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2062 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
2063 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2064 errorCallback(error as OCPPError, false)
2065 } else {
2066 // Remove the request from the cache in case of error at response handling
2067 this.requests.delete(messageId)
2068 }
2069 break
2070 }
2071 if (!(error instanceof OCPPError)) {
2072 logger.warn(
2073 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2074 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2075 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2076 }' message '${data.toString()}' handling is not an OCPPError:`,
2077 error
2078 )
2079 }
2080 logger.error(
2081 `${this.logPrefix()} Incoming OCPP command '${
2082 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2083 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2084 }' message '${data.toString()}'${
2085 this.requests.has(messageId)
2086 ? ` matching cached request '${JSON.stringify(this.getCachedRequest(messageType, messageId))}'`
2087 : ''
2088 } processing error:`,
2089 error
2090 )
2091 }
2092 }
2093
2094 private onPing (): void {
2095 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2096 }
2097
2098 private onPong (): void {
2099 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2100 }
2101
2102 private onError (error: WSError): void {
2103 this.closeWSConnection()
2104 logger.error(`${this.logPrefix()} WebSocket error:`, error)
2105 }
2106
2107 private getEnergyActiveImportRegister (
2108 connectorStatus: ConnectorStatus | undefined,
2109 rounded = false
2110 ): number {
2111 if (this.stationInfo?.meteringPerTransaction === true) {
2112 return (
2113 (rounded
2114 ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
2115 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
2116 : undefined
2117 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2118 )
2119 }
2120 return (
2121 (rounded
2122 ? connectorStatus?.energyActiveImportRegisterValue != null
2123 ? Math.round(connectorStatus.energyActiveImportRegisterValue)
2124 : undefined
2125 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2126 )
2127 }
2128
2129 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2130 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2131 return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
2132 }
2133
2134 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
2135 if (this.hasEvses) {
2136 for (const [evseId, evseStatus] of this.evses) {
2137 if (evseId === 0) {
2138 continue
2139 }
2140 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2141 if (connectorStatus.transactionStarted === true) {
2142 await this.stopTransactionOnConnector(connectorId, reason)
2143 }
2144 }
2145 }
2146 } else {
2147 for (const connectorId of this.connectors.keys()) {
2148 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2149 await this.stopTransactionOnConnector(connectorId, reason)
2150 }
2151 }
2152 }
2153 }
2154
2155 // 0 for disabling
2156 private getConnectionTimeout (): number {
2157 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
2158 return convertToInt(
2159 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
2160 Constants.DEFAULT_CONNECTION_TIMEOUT
2161 )
2162 }
2163 return Constants.DEFAULT_CONNECTION_TIMEOUT
2164 }
2165
2166 private getPowerDivider (): number {
2167 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
2168 if (this.stationInfo?.powerSharedByConnectors === true) {
2169 powerDivider = this.getNumberOfRunningTransactions()
2170 }
2171 return powerDivider
2172 }
2173
2174 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2175 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2176 const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
2177 switch (this.getCurrentOutType(stationInfo)) {
2178 case CurrentType.AC:
2179 return ACElectricUtils.amperagePerPhaseFromPower(
2180 this.getNumberOfPhases(stationInfo),
2181 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2182 this.getVoltageOut(stationInfo)
2183 )
2184 case CurrentType.DC:
2185 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
2186 }
2187 }
2188
2189 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
2190 return (
2191 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2192 (stationInfo ?? this.stationInfo!).currentOutType ??
2193 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2194 Constants.DEFAULT_STATION_INFO.currentOutType!
2195 )
2196 }
2197
2198 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
2199 return (
2200 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2201 (stationInfo ?? this.stationInfo!).voltageOut ??
2202 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
2203 )
2204 }
2205
2206 private getAmperageLimitation (): number | undefined {
2207 if (
2208 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2209 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
2210 ) {
2211 return (
2212 convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
2213 getAmperageLimitationUnitDivider(this.stationInfo)
2214 )
2215 }
2216 }
2217
2218 private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
2219 if (this.stationInfo?.autoRegister === true) {
2220 await this.ocppRequestService.requestHandler<
2221 BootNotificationRequest,
2222 BootNotificationResponse
2223 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2224 skipBufferingOnError: true
2225 })
2226 }
2227 // Start WebSocket ping
2228 this.startWebSocketPing()
2229 // Start heartbeat
2230 this.startHeartbeat()
2231 // Initialize connectors status
2232 if (this.hasEvses) {
2233 for (const [evseId, evseStatus] of this.evses) {
2234 if (evseId > 0) {
2235 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2236 const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus)
2237 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId)
2238 }
2239 }
2240 }
2241 } else {
2242 for (const connectorId of this.connectors.keys()) {
2243 if (connectorId > 0) {
2244 const connectorBootStatus = getBootConnectorStatus(
2245 this,
2246 connectorId,
2247 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2248 this.getConnectorStatus(connectorId)!
2249 )
2250 await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus)
2251 }
2252 }
2253 }
2254 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2255 await this.ocppRequestService.requestHandler<
2256 FirmwareStatusNotificationRequest,
2257 FirmwareStatusNotificationResponse
2258 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2259 status: FirmwareStatus.Installed
2260 })
2261 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
2262 }
2263
2264 // Start the ATG
2265 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2266 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
2267 }
2268 this.flushMessageBuffer()
2269 }
2270
2271 private internalStopMessageSequence (): void {
2272 // Stop WebSocket ping
2273 this.stopWebSocketPing()
2274 // Stop heartbeat
2275 this.stopHeartbeat()
2276 // Stop the ATG
2277 if (this.automaticTransactionGenerator?.started === true) {
2278 this.stopAutomaticTransactionGenerator()
2279 }
2280 }
2281
2282 private async stopMessageSequence (
2283 reason?: StopTransactionReason,
2284 stopTransactions?: boolean
2285 ): Promise<void> {
2286 this.internalStopMessageSequence()
2287 // Stop ongoing transactions
2288 stopTransactions === true && (await this.stopRunningTransactions(reason))
2289 if (this.hasEvses) {
2290 for (const [evseId, evseStatus] of this.evses) {
2291 if (evseId > 0) {
2292 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2293 await sendAndSetConnectorStatus(
2294 this,
2295 connectorId,
2296 ConnectorStatusEnum.Unavailable,
2297 evseId
2298 )
2299 delete connectorStatus.status
2300 }
2301 }
2302 }
2303 } else {
2304 for (const connectorId of this.connectors.keys()) {
2305 if (connectorId > 0) {
2306 await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
2307 delete this.getConnectorStatus(connectorId)?.status
2308 }
2309 }
2310 }
2311 }
2312
2313 private startWebSocketPing (): void {
2314 const webSocketPingInterval =
2315 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
2316 ? convertToInt(
2317 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
2318 )
2319 : 0
2320 if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
2321 this.wsPingSetInterval = setInterval(() => {
2322 if (this.isWebSocketConnectionOpened()) {
2323 this.wsConnection?.ping()
2324 }
2325 }, secondsToMilliseconds(webSocketPingInterval))
2326 logger.info(
2327 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2328 webSocketPingInterval
2329 )}`
2330 )
2331 } else if (this.wsPingSetInterval != null) {
2332 logger.info(
2333 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2334 webSocketPingInterval
2335 )}`
2336 )
2337 } else {
2338 logger.error(
2339 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2340 )
2341 }
2342 }
2343
2344 private stopWebSocketPing (): void {
2345 if (this.wsPingSetInterval != null) {
2346 clearInterval(this.wsPingSetInterval)
2347 delete this.wsPingSetInterval
2348 }
2349 }
2350
2351 private getConfiguredSupervisionUrl (): URL {
2352 let configuredSupervisionUrl: string
2353 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
2354 if (isNotEmptyArray(supervisionUrls)) {
2355 let configuredSupervisionUrlIndex: number
2356 switch (Configuration.getSupervisionUrlDistribution()) {
2357 case SupervisionUrlDistribution.RANDOM:
2358 configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
2359 break
2360 case SupervisionUrlDistribution.ROUND_ROBIN:
2361 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2362 default:
2363 !Object.values(SupervisionUrlDistribution).includes(
2364 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2365 Configuration.getSupervisionUrlDistribution()!
2366 ) &&
2367 logger.warn(
2368 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2369 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2370 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2371 }'`
2372 )
2373 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
2374 break
2375 }
2376 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
2377 } else {
2378 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2379 configuredSupervisionUrl = supervisionUrls!
2380 }
2381 if (isNotEmptyString(configuredSupervisionUrl)) {
2382 return new URL(configuredSupervisionUrl)
2383 }
2384 const errorMsg = 'No supervision url(s) configured'
2385 logger.error(`${this.logPrefix()} ${errorMsg}`)
2386 throw new BaseError(errorMsg)
2387 }
2388
2389 private stopHeartbeat (): void {
2390 if (this.heartbeatSetInterval != null) {
2391 clearInterval(this.heartbeatSetInterval)
2392 delete this.heartbeatSetInterval
2393 }
2394 }
2395
2396 private terminateWSConnection (): void {
2397 if (this.isWebSocketConnectionOpened()) {
2398 this.wsConnection?.terminate()
2399 this.wsConnection = null
2400 }
2401 }
2402
2403 private async reconnect (): Promise<void> {
2404 if (
2405 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2406 this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
2407 this.stationInfo?.autoReconnectMaxRetries === -1
2408 ) {
2409 this.wsConnectionRetried = true
2410 ++this.wsConnectionRetryCount
2411 const reconnectDelay =
2412 this.stationInfo?.reconnectExponentialDelay === true
2413 ? exponentialDelay(this.wsConnectionRetryCount)
2414 : secondsToMilliseconds(this.getConnectionTimeout())
2415 const reconnectDelayWithdraw = 1000
2416 const reconnectTimeout =
2417 reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
2418 logger.error(
2419 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2420 reconnectDelay,
2421 2
2422 )}ms, timeout ${reconnectTimeout}ms`
2423 )
2424 await sleep(reconnectDelay)
2425 logger.error(
2426 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2427 )
2428 this.openWSConnection(
2429 {
2430 handshakeTimeout: reconnectTimeout
2431 },
2432 { closeOpened: true }
2433 )
2434 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
2435 logger.error(
2436 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2437 this.wsConnectionRetryCount
2438 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
2439 )
2440 }
2441 }
2442 }