fix: use lodash merge to deep merge objects
[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 'lodash-es'
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 HeartbeatRequest,
92 type HeartbeatResponse,
93 type IncomingRequest,
94 type IncomingRequestCommand,
95 MessageType,
96 MeterValueMeasurand,
97 type MeterValuesRequest,
98 type MeterValuesResponse,
99 OCPPVersion,
100 type OutgoingRequest,
101 PowerUnits,
102 RegistrationStatusEnumType,
103 RequestCommand,
104 type Reservation,
105 type ReservationKey,
106 ReservationTerminationReason,
107 type Response,
108 StandardParametersKey,
109 type Status,
110 type StopTransactionReason,
111 type StopTransactionRequest,
112 type StopTransactionResponse,
113 SupervisionUrlDistribution,
114 SupportedFeatureProfiles,
115 type Voltage,
116 type WSError,
117 WebSocketCloseEventStatusCode,
118 type WsOptions
119 } from '../types/index.js'
120 import {
121 ACElectricUtils,
122 AsyncLock,
123 AsyncLockType,
124 Configuration,
125 Constants,
126 DCElectricUtils,
127 buildAddedMessage,
128 buildChargingStationAutomaticTransactionGeneratorConfiguration,
129 buildConnectorsStatus,
130 buildDeletedMessage,
131 buildEvsesStatus,
132 buildStartedMessage,
133 buildStoppedMessage,
134 buildUpdatedMessage,
135 clone,
136 convertToBoolean,
137 convertToDate,
138 convertToInt,
139 exponentialDelay,
140 formatDurationMilliSeconds,
141 formatDurationSeconds,
142 getRandomInteger,
143 getWebSocketCloseEventStatusString,
144 handleFileException,
145 isNotEmptyArray,
146 isNotEmptyString,
147 logPrefix,
148 logger,
149 min,
150 once,
151 roundTo,
152 secureRandom,
153 sleep,
154 watchJsonFile
155 } from '../utils/index.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 setSupervisionUrls (urls: string | string[], saveStationInfo = true): void {
538 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
539 this.stationInfo!.supervisionUrls = urls
540 if (saveStationInfo) {
541 this.saveStationInfo()
542 }
543 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
544 }
545
546 public setSupervisionUrl (url: string): void {
547 if (
548 this.stationInfo?.supervisionUrlOcppConfiguration === true &&
549 isNotEmptyString(this.stationInfo.supervisionUrlOcppKey)
550 ) {
551 setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
552 } else {
553 this.setSupervisionUrls(url)
554 }
555 }
556
557 public startHeartbeat (): void {
558 if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval == null) {
559 this.heartbeatSetInterval = setInterval(() => {
560 this.ocppRequestService
561 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
562 .catch(error => {
563 logger.error(
564 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
565 error
566 )
567 })
568 }, this.getHeartbeatInterval())
569 logger.info(
570 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
571 this.getHeartbeatInterval()
572 )}`
573 )
574 } else if (this.heartbeatSetInterval != null) {
575 logger.info(
576 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
577 this.getHeartbeatInterval()
578 )}`
579 )
580 } else {
581 logger.error(
582 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
583 )
584 }
585 }
586
587 public restartHeartbeat (): void {
588 // Stop heartbeat
589 this.stopHeartbeat()
590 // Start heartbeat
591 this.startHeartbeat()
592 }
593
594 public restartWebSocketPing (): void {
595 // Stop WebSocket ping
596 this.stopWebSocketPing()
597 // Start WebSocket ping
598 this.startWebSocketPing()
599 }
600
601 public startMeterValues (connectorId: number, interval: number): void {
602 if (connectorId === 0) {
603 logger.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`)
604 return
605 }
606 const connectorStatus = this.getConnectorStatus(connectorId)
607 if (connectorStatus == null) {
608 logger.error(
609 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
610 ${connectorId}`
611 )
612 return
613 }
614 if (connectorStatus.transactionStarted === false) {
615 logger.error(
616 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
617 )
618 return
619 } else if (
620 connectorStatus.transactionStarted === true &&
621 connectorStatus.transactionId == null
622 ) {
623 logger.error(
624 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
625 )
626 return
627 }
628 if (interval > 0) {
629 connectorStatus.transactionSetInterval = setInterval(() => {
630 const meterValue = buildMeterValue(
631 this,
632 connectorId,
633 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
634 connectorStatus.transactionId!,
635 interval
636 )
637 this.ocppRequestService
638 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
639 this,
640 RequestCommand.METER_VALUES,
641 {
642 connectorId,
643 transactionId: connectorStatus.transactionId,
644 meterValue: [meterValue]
645 }
646 )
647 .catch(error => {
648 logger.error(
649 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
650 error
651 )
652 })
653 }, interval)
654 } else {
655 logger.error(
656 `${this.logPrefix()} Charging station ${
657 StandardParametersKey.MeterValueSampleInterval
658 } configuration set to ${interval}, not sending MeterValues`
659 )
660 }
661 }
662
663 public stopMeterValues (connectorId: number): void {
664 const connectorStatus = this.getConnectorStatus(connectorId)
665 if (connectorStatus?.transactionSetInterval != null) {
666 clearInterval(connectorStatus.transactionSetInterval)
667 }
668 }
669
670 private add (): void {
671 this.emit(ChargingStationEvents.added)
672 }
673
674 public async delete (deleteConfiguration = true): Promise<void> {
675 if (this.started) {
676 await this.stop()
677 }
678 AutomaticTransactionGenerator.deleteInstance(this)
679 PerformanceStatistics.deleteInstance(this.stationInfo?.hashId)
680 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
681 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
682 this.requests.clear()
683 this.connectors.clear()
684 this.evses.clear()
685 this.templateFileWatcher?.unref()
686 deleteConfiguration && rmSync(this.configurationFile, { force: true })
687 this.chargingStationWorkerBroadcastChannel.unref()
688 this.emit(ChargingStationEvents.deleted)
689 this.removeAllListeners()
690 }
691
692 public start (): void {
693 if (!this.started) {
694 if (!this.starting) {
695 this.starting = true
696 if (this.stationInfo?.enableStatistics === true) {
697 this.performanceStatistics?.start()
698 }
699 this.openWSConnection()
700 // Monitor charging station template file
701 this.templateFileWatcher = watchJsonFile(
702 this.templateFile,
703 FileType.ChargingStationTemplate,
704 this.logPrefix(),
705 undefined,
706 (event, filename): void => {
707 if (isNotEmptyString(filename) && event === 'change') {
708 try {
709 logger.debug(
710 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
711 this.templateFile
712 } file have changed, reload`
713 )
714 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
715 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
716 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
717 // Initialize
718 this.initialize()
719 // Restart the ATG
720 const ATGStarted = this.automaticTransactionGenerator?.started
721 if (ATGStarted === true) {
722 this.stopAutomaticTransactionGenerator()
723 }
724 delete this.automaticTransactionGeneratorConfiguration
725 if (
726 this.getAutomaticTransactionGeneratorConfiguration()?.enable === true &&
727 ATGStarted === true
728 ) {
729 this.startAutomaticTransactionGenerator(undefined, true)
730 }
731 if (this.stationInfo?.enableStatistics === true) {
732 this.performanceStatistics?.restart()
733 } else {
734 this.performanceStatistics?.stop()
735 }
736 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
737 } catch (error) {
738 logger.error(
739 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
740 error
741 )
742 }
743 }
744 }
745 )
746 this.started = true
747 this.emit(ChargingStationEvents.started)
748 this.starting = false
749 } else {
750 logger.warn(`${this.logPrefix()} Charging station is already starting...`)
751 }
752 } else {
753 logger.warn(`${this.logPrefix()} Charging station is already started...`)
754 }
755 }
756
757 public async stop (
758 reason?: StopTransactionReason,
759 stopTransactions = this.stationInfo?.stopTransactionsOnStopped
760 ): Promise<void> {
761 if (this.started) {
762 if (!this.stopping) {
763 this.stopping = true
764 await this.stopMessageSequence(reason, stopTransactions)
765 this.closeWSConnection()
766 if (this.stationInfo?.enableStatistics === true) {
767 this.performanceStatistics?.stop()
768 }
769 this.templateFileWatcher?.close()
770 delete this.bootNotificationResponse
771 this.started = false
772 this.saveConfiguration()
773 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
774 this.emit(ChargingStationEvents.stopped)
775 this.stopping = false
776 } else {
777 logger.warn(`${this.logPrefix()} Charging station is already stopping...`)
778 }
779 } else {
780 logger.warn(`${this.logPrefix()} Charging station is already stopped...`)
781 }
782 }
783
784 public async reset (reason?: StopTransactionReason): Promise<void> {
785 await this.stop(reason)
786 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
787 await sleep(this.stationInfo!.resetTime!)
788 this.initialize()
789 this.start()
790 }
791
792 public saveOcppConfiguration (): void {
793 if (this.stationInfo?.ocppPersistentConfiguration === true) {
794 this.saveConfiguration()
795 }
796 }
797
798 public bufferMessage (message: string): void {
799 this.messageBuffer.add(message)
800 this.setIntervalFlushMessageBuffer()
801 }
802
803 public openWSConnection (
804 options?: WsOptions,
805 params?: { closeOpened?: boolean, terminateOpened?: boolean }
806 ): void {
807 options = {
808 handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
809 ...this.stationInfo?.wsOptions,
810 ...options
811 }
812 params = { ...{ closeOpened: false, terminateOpened: false }, ...params }
813 if (!checkChargingStation(this, this.logPrefix())) {
814 return
815 }
816 if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) {
817 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
818 }
819 if (params.closeOpened === true) {
820 this.closeWSConnection()
821 }
822 if (params.terminateOpened === true) {
823 this.terminateWSConnection()
824 }
825
826 if (this.isWebSocketConnectionOpened()) {
827 logger.warn(
828 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
829 )
830 return
831 }
832
833 logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
834
835 this.wsConnection = new WebSocket(
836 this.wsConnectionUrl,
837 `ocpp${this.stationInfo?.ocppVersion}`,
838 options
839 )
840
841 // Handle WebSocket message
842 this.wsConnection.on('message', data => {
843 this.onMessage(data).catch(Constants.EMPTY_FUNCTION)
844 })
845 // Handle WebSocket error
846 this.wsConnection.on('error', this.onError.bind(this))
847 // Handle WebSocket close
848 this.wsConnection.on('close', this.onClose.bind(this))
849 // Handle WebSocket open
850 this.wsConnection.on('open', () => {
851 this.onOpen().catch(error =>
852 logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
853 )
854 })
855 // Handle WebSocket ping
856 this.wsConnection.on('ping', this.onPing.bind(this))
857 // Handle WebSocket pong
858 this.wsConnection.on('pong', this.onPong.bind(this))
859 }
860
861 public closeWSConnection (): void {
862 if (this.isWebSocketConnectionOpened()) {
863 this.wsConnection?.close()
864 this.wsConnection = null
865 }
866 }
867
868 public getAutomaticTransactionGeneratorConfiguration ():
869 | AutomaticTransactionGeneratorConfiguration
870 | undefined {
871 if (this.automaticTransactionGeneratorConfiguration == null) {
872 let automaticTransactionGeneratorConfiguration:
873 | AutomaticTransactionGeneratorConfiguration
874 | undefined
875 const stationTemplate = this.getTemplateFromFile()
876 const stationConfiguration = this.getConfigurationFromFile()
877 if (
878 this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
879 stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
880 stationConfiguration?.automaticTransactionGenerator != null
881 ) {
882 automaticTransactionGeneratorConfiguration =
883 stationConfiguration.automaticTransactionGenerator
884 } else {
885 automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
886 }
887 this.automaticTransactionGeneratorConfiguration = {
888 ...Constants.DEFAULT_ATG_CONFIGURATION,
889 ...automaticTransactionGeneratorConfiguration
890 }
891 }
892 return this.automaticTransactionGeneratorConfiguration
893 }
894
895 public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
896 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
897 }
898
899 public startAutomaticTransactionGenerator (
900 connectorIds?: number[],
901 stopAbsoluteDuration?: boolean
902 ): void {
903 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
904 if (isNotEmptyArray(connectorIds)) {
905 for (const connectorId of connectorIds) {
906 this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration)
907 }
908 } else {
909 this.automaticTransactionGenerator?.start(stopAbsoluteDuration)
910 }
911 this.saveAutomaticTransactionGeneratorConfiguration()
912 this.emit(ChargingStationEvents.updated)
913 }
914
915 public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
916 if (isNotEmptyArray(connectorIds)) {
917 for (const connectorId of connectorIds) {
918 this.automaticTransactionGenerator?.stopConnector(connectorId)
919 }
920 } else {
921 this.automaticTransactionGenerator?.stop()
922 }
923 this.saveAutomaticTransactionGeneratorConfiguration()
924 this.emit(ChargingStationEvents.updated)
925 }
926
927 public async stopTransactionOnConnector (
928 connectorId: number,
929 reason?: StopTransactionReason
930 ): Promise<StopTransactionResponse> {
931 const transactionId = this.getConnectorStatus(connectorId)?.transactionId
932 if (
933 this.stationInfo?.beginEndMeterValues === true &&
934 this.stationInfo.ocppStrictCompliance === true &&
935 this.stationInfo.outOfOrderEndMeterValues === false
936 ) {
937 const transactionEndMeterValue = buildTransactionEndMeterValue(
938 this,
939 connectorId,
940 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
941 )
942 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
943 this,
944 RequestCommand.METER_VALUES,
945 {
946 connectorId,
947 transactionId,
948 meterValue: [transactionEndMeterValue]
949 }
950 )
951 }
952 return await this.ocppRequestService.requestHandler<
953 StopTransactionRequest,
954 StopTransactionResponse
955 >(this, RequestCommand.STOP_TRANSACTION, {
956 transactionId,
957 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
958 ...(reason != null && { reason })
959 })
960 }
961
962 public getReserveConnectorZeroSupported (): boolean {
963 return convertToBoolean(
964 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
965 getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
966 )
967 }
968
969 public async addReservation (reservation: Reservation): Promise<void> {
970 const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
971 if (reservationFound != null) {
972 await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
973 }
974 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
975 this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
976 await sendAndSetConnectorStatus(
977 this,
978 reservation.connectorId,
979 ConnectorStatusEnum.Reserved,
980 undefined,
981 { send: reservation.connectorId !== 0 }
982 )
983 }
984
985 public async removeReservation (
986 reservation: Reservation,
987 reason: ReservationTerminationReason
988 ): Promise<void> {
989 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
990 const connector = this.getConnectorStatus(reservation.connectorId)!
991 switch (reason) {
992 case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
993 case ReservationTerminationReason.TRANSACTION_STARTED:
994 delete connector.reservation
995 break
996 case ReservationTerminationReason.RESERVATION_CANCELED:
997 case ReservationTerminationReason.REPLACE_EXISTING:
998 case ReservationTerminationReason.EXPIRED:
999 await sendAndSetConnectorStatus(
1000 this,
1001 reservation.connectorId,
1002 ConnectorStatusEnum.Available,
1003 undefined,
1004 { send: reservation.connectorId !== 0 }
1005 )
1006 delete connector.reservation
1007 break
1008 default:
1009 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1010 throw new BaseError(`Unknown reservation termination reason '${reason}'`)
1011 }
1012 }
1013
1014 public getReservationBy (
1015 filterKey: ReservationKey,
1016 value: number | string
1017 ): Reservation | undefined {
1018 if (this.hasEvses) {
1019 for (const evseStatus of this.evses.values()) {
1020 for (const connectorStatus of evseStatus.connectors.values()) {
1021 if (connectorStatus.reservation?.[filterKey] === value) {
1022 return connectorStatus.reservation
1023 }
1024 }
1025 }
1026 } else {
1027 for (const connectorStatus of this.connectors.values()) {
1028 if (connectorStatus.reservation?.[filterKey] === value) {
1029 return connectorStatus.reservation
1030 }
1031 }
1032 }
1033 }
1034
1035 public isConnectorReservable (
1036 reservationId: number,
1037 idTag?: string,
1038 connectorId?: number
1039 ): boolean {
1040 const reservation = this.getReservationBy('reservationId', reservationId)
1041 const reservationExists = reservation != null && !hasReservationExpired(reservation)
1042 if (arguments.length === 1) {
1043 return !reservationExists
1044 } else if (arguments.length > 1) {
1045 const userReservation = idTag != null ? this.getReservationBy('idTag', idTag) : undefined
1046 const userReservationExists =
1047 userReservation != null && !hasReservationExpired(userReservation)
1048 const notConnectorZero = connectorId == null ? true : connectorId > 0
1049 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
1050 return (
1051 !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
1052 )
1053 }
1054 return false
1055 }
1056
1057 private setIntervalFlushMessageBuffer (): void {
1058 if (this.flushMessageBufferSetInterval == null) {
1059 this.flushMessageBufferSetInterval = setInterval(() => {
1060 if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
1061 this.flushMessageBuffer()
1062 }
1063 if (this.messageBuffer.size === 0) {
1064 this.clearIntervalFlushMessageBuffer()
1065 }
1066 }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
1067 }
1068 }
1069
1070 private clearIntervalFlushMessageBuffer (): void {
1071 if (this.flushMessageBufferSetInterval != null) {
1072 clearInterval(this.flushMessageBufferSetInterval)
1073 delete this.flushMessageBufferSetInterval
1074 }
1075 }
1076
1077 private getNumberOfReservableConnectors (): number {
1078 let numberOfReservableConnectors = 0
1079 if (this.hasEvses) {
1080 for (const evseStatus of this.evses.values()) {
1081 numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
1082 }
1083 } else {
1084 numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
1085 }
1086 return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
1087 }
1088
1089 private getNumberOfReservationsOnConnectorZero (): number {
1090 if (
1091 (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
1092 (!this.hasEvses && this.connectors.get(0)?.reservation != null)
1093 ) {
1094 return 1
1095 }
1096 return 0
1097 }
1098
1099 private flushMessageBuffer (): void {
1100 if (this.messageBuffer.size > 0) {
1101 for (const message of this.messageBuffer.values()) {
1102 let beginId: string | undefined
1103 let commandName: RequestCommand | undefined
1104 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse
1105 const isRequest = messageType === MessageType.CALL_MESSAGE
1106 if (isRequest) {
1107 [, , commandName] = JSON.parse(message) as OutgoingRequest
1108 beginId = PerformanceStatistics.beginMeasure(commandName)
1109 }
1110 this.wsConnection?.send(message, (error?: Error) => {
1111 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1112 isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
1113 if (error == null) {
1114 logger.debug(
1115 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1116 messageType
1117 )} OCPP message sent '${JSON.stringify(message)}'`
1118 )
1119 this.messageBuffer.delete(message)
1120 } else {
1121 logger.debug(
1122 `${this.logPrefix()} >> Buffered ${getMessageTypeString(
1123 messageType
1124 )} OCPP message '${JSON.stringify(message)}' send failed:`,
1125 error
1126 )
1127 }
1128 })
1129 }
1130 }
1131 }
1132
1133 private getTemplateFromFile (): ChargingStationTemplate | undefined {
1134 let template: ChargingStationTemplate | undefined
1135 try {
1136 if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
1137 template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
1138 } else {
1139 const measureId = `${FileType.ChargingStationTemplate} read`
1140 const beginId = PerformanceStatistics.beginMeasure(measureId)
1141 template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
1142 PerformanceStatistics.endMeasure(measureId, beginId)
1143 template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
1144 .update(JSON.stringify(template))
1145 .digest('hex')
1146 this.sharedLRUCache.setChargingStationTemplate(template)
1147 this.templateFileHash = template.templateHash
1148 }
1149 } catch (error) {
1150 handleFileException(
1151 this.templateFile,
1152 FileType.ChargingStationTemplate,
1153 error as NodeJS.ErrnoException,
1154 this.logPrefix()
1155 )
1156 }
1157 return template
1158 }
1159
1160 private getStationInfoFromTemplate (): ChargingStationInfo {
1161 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1162 const stationTemplate = this.getTemplateFromFile()!
1163 checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
1164 const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this)
1165 warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
1166 if (stationTemplate.Connectors != null) {
1167 checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
1168 }
1169 const stationInfo = stationTemplateToStationInfo(stationTemplate)
1170 stationInfo.hashId = getHashId(this.index, stationTemplate)
1171 stationInfo.templateIndex = this.index
1172 stationInfo.templateName = parse(this.templateFile).name
1173 stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
1174 createSerialNumber(stationTemplate, stationInfo)
1175 stationInfo.voltageOut = this.getVoltageOut(stationInfo)
1176 if (isNotEmptyArray(stationTemplate.power)) {
1177 const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
1178 stationInfo.maximumPower =
1179 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1180 ? stationTemplate.power[powerArrayRandomIndex] * 1000
1181 : stationTemplate.power[powerArrayRandomIndex]
1182 } else {
1183 stationInfo.maximumPower =
1184 stationTemplate.powerUnit === PowerUnits.KILO_WATT
1185 ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1186 stationTemplate.power! * 1000
1187 : stationTemplate.power
1188 }
1189 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
1190 if (
1191 isNotEmptyString(stationInfo.firmwareVersionPattern) &&
1192 isNotEmptyString(stationInfo.firmwareVersion) &&
1193 !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
1194 ) {
1195 logger.warn(
1196 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1197 this.templateFile
1198 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1199 )
1200 }
1201 stationInfo.firmwareUpgrade = merge(
1202 {
1203 versionUpgrade: {
1204 step: 1
1205 },
1206 reset: true
1207 },
1208 stationTemplate.firmwareUpgrade ?? {}
1209 )
1210 if (stationTemplate.resetTime != null) {
1211 stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
1212 }
1213 return stationInfo
1214 }
1215
1216 private getStationInfoFromFile (
1217 stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO
1218 .stationInfoPersistentConfiguration
1219 ): ChargingStationInfo | undefined {
1220 let stationInfo: ChargingStationInfo | undefined
1221 if (stationInfoPersistentConfiguration === true) {
1222 stationInfo = this.getConfigurationFromFile()?.stationInfo
1223 if (stationInfo != null) {
1224 delete stationInfo.infoHash
1225 delete (stationInfo as ChargingStationTemplate).numberOfConnectors
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(
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 }