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