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