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