refactor: cleanup connection retries logic
[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-unnecessary-condition
1847 if (this.bootNotificationResponse?.currentTime != null) {
1848 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1849 this.bootNotificationResponse.currentTime = convertToDate(
1850 this.bootNotificationResponse.currentTime
1851 )!
1852 }
1853 if (!this.isRegistered()) {
1854 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
1855 await sleep(
1856 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1857 this.bootNotificationResponse?.interval != null
1858 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1859 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1860 )
1861 }
1862 } while (
1863 !this.isRegistered() &&
1864 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1865 (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
1866 this.stationInfo?.registrationMaxRetries === -1)
1867 )
1868 }
1869 if (!this.isRegistered()) {
1870 logger.error(
1871 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1872 this.stationInfo?.registrationMaxRetries
1873 })`
1874 )
1875 }
1876 this.emit(ChargingStationEvents.updated)
1877 } else {
1878 logger.warn(
1879 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
1880 )
1881 }
1882 }
1883
1884 private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
1885 this.emit(ChargingStationEvents.disconnected)
1886 this.emit(ChargingStationEvents.updated)
1887 switch (code) {
1888 // Normal close
1889 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1890 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1891 logger.info(
1892 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1893 code
1894 )}' and reason '${reason.toString()}'`
1895 )
1896 this.wsConnectionRetryCount = 0
1897 break
1898 // Abnormal close
1899 default:
1900 logger.error(
1901 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1902 code
1903 )}' and reason '${reason.toString()}'`
1904 )
1905 this.started &&
1906 this.reconnect()
1907 .then(() => {
1908 this.emit(ChargingStationEvents.updated)
1909 })
1910 .catch((error: unknown) =>
1911 logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
1912 )
1913 break
1914 }
1915 }
1916
1917 private getCachedRequest (
1918 messageType: MessageType | undefined,
1919 messageId: string
1920 ): CachedRequest | undefined {
1921 const cachedRequest = this.requests.get(messageId)
1922 if (Array.isArray(cachedRequest)) {
1923 return cachedRequest
1924 }
1925 throw new OCPPError(
1926 ErrorType.PROTOCOL_ERROR,
1927 `Cached request for message id ${messageId} ${getMessageTypeString(
1928 messageType
1929 )} is not an array`,
1930 undefined,
1931 cachedRequest
1932 )
1933 }
1934
1935 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1936 const [messageType, messageId, commandName, commandPayload] = request
1937 if (this.stationInfo?.enableStatistics === true) {
1938 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
1939 }
1940 logger.debug(
1941 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1942 request
1943 )}`
1944 )
1945 // Process the message
1946 await this.ocppIncomingRequestService.incomingRequestHandler(
1947 this,
1948 messageId,
1949 commandName,
1950 commandPayload
1951 )
1952 this.emit(ChargingStationEvents.updated)
1953 }
1954
1955 private handleResponseMessage (response: Response): void {
1956 const [messageType, messageId, commandPayload] = response
1957 if (!this.requests.has(messageId)) {
1958 // Error
1959 throw new OCPPError(
1960 ErrorType.INTERNAL_ERROR,
1961 `Response for unknown message id ${messageId}`,
1962 undefined,
1963 commandPayload
1964 )
1965 }
1966 // Respond
1967 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1968 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1969 messageType,
1970 messageId
1971 )!
1972 logger.debug(
1973 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1974 response
1975 )}`
1976 )
1977 responseCallback(commandPayload, requestPayload)
1978 }
1979
1980 private handleErrorMessage (errorResponse: ErrorResponse): void {
1981 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1982 if (!this.requests.has(messageId)) {
1983 // Error
1984 throw new OCPPError(
1985 ErrorType.INTERNAL_ERROR,
1986 `Error response for unknown message id ${messageId}`,
1987 undefined,
1988 { errorType, errorMessage, errorDetails }
1989 )
1990 }
1991 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1992 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1993 logger.debug(
1994 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1995 errorResponse
1996 )}`
1997 )
1998 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
1999 }
2000
2001 private async onMessage (data: RawData): Promise<void> {
2002 let request: IncomingRequest | Response | ErrorResponse | undefined
2003 let messageType: MessageType | undefined
2004 let errorMsg: string
2005 try {
2006 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2007 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
2008 if (Array.isArray(request)) {
2009 [messageType] = request
2010 // Check the type of message
2011 switch (messageType) {
2012 // Incoming Message
2013 case MessageType.CALL_MESSAGE:
2014 await this.handleIncomingMessage(request as IncomingRequest)
2015 break
2016 // Response Message
2017 case MessageType.CALL_RESULT_MESSAGE:
2018 this.handleResponseMessage(request as Response)
2019 break
2020 // Error Message
2021 case MessageType.CALL_ERROR_MESSAGE:
2022 this.handleErrorMessage(request as ErrorResponse)
2023 break
2024 // Unknown Message
2025 default:
2026 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2027 errorMsg = `Wrong message type ${messageType}`
2028 logger.error(`${this.logPrefix()} ${errorMsg}`)
2029 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
2030 }
2031 } else {
2032 throw new OCPPError(
2033 ErrorType.PROTOCOL_ERROR,
2034 'Incoming message is not an array',
2035 undefined,
2036 {
2037 request
2038 }
2039 )
2040 }
2041 } catch (error) {
2042 if (!Array.isArray(request)) {
2043 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
2044 return
2045 }
2046 let commandName: IncomingRequestCommand | undefined
2047 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
2048 let errorCallback: ErrorCallback
2049 const [, messageId] = request
2050 switch (messageType) {
2051 case MessageType.CALL_MESSAGE:
2052 [, , commandName] = request as IncomingRequest
2053 // Send error
2054 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
2055 break
2056 case MessageType.CALL_RESULT_MESSAGE:
2057 case MessageType.CALL_ERROR_MESSAGE:
2058 if (this.requests.has(messageId)) {
2059 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2060 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
2061 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2062 errorCallback(error as OCPPError, false)
2063 } else {
2064 // Remove the request from the cache in case of error at response handling
2065 this.requests.delete(messageId)
2066 }
2067 break
2068 }
2069 if (!(error instanceof OCPPError)) {
2070 logger.warn(
2071 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2072 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2073 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2074 }' message '${data.toString()}' handling is not an OCPPError:`,
2075 error
2076 )
2077 }
2078 logger.error(
2079 `${this.logPrefix()} Incoming OCPP command '${
2080 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2081 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2082 }' message '${data.toString()}'${
2083 this.requests.has(messageId)
2084 ? ` matching cached request '${JSON.stringify(
2085 this.getCachedRequest(messageType, messageId)
2086 )}'`
2087 : ''
2088 } processing error:`,
2089 error
2090 )
2091 }
2092 }
2093
2094 private onPing (): void {
2095 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2096 }
2097
2098 private onPong (): void {
2099 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2100 }
2101
2102 private onError (error: WSError): void {
2103 this.closeWSConnection()
2104 logger.error(`${this.logPrefix()} WebSocket error:`, error)
2105 }
2106
2107 private getEnergyActiveImportRegister (
2108 connectorStatus: ConnectorStatus | undefined,
2109 rounded = false
2110 ): number {
2111 if (this.stationInfo?.meteringPerTransaction === true) {
2112 return (
2113 (rounded
2114 ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
2115 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
2116 : undefined
2117 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2118 )
2119 }
2120 return (
2121 (rounded
2122 ? connectorStatus?.energyActiveImportRegisterValue != null
2123 ? Math.round(connectorStatus.energyActiveImportRegisterValue)
2124 : undefined
2125 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2126 )
2127 }
2128
2129 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2130 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2131 return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
2132 }
2133
2134 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
2135 if (this.hasEvses) {
2136 for (const [evseId, evseStatus] of this.evses) {
2137 if (evseId === 0) {
2138 continue
2139 }
2140 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2141 if (connectorStatus.transactionStarted === true) {
2142 await this.stopTransactionOnConnector(connectorId, reason)
2143 }
2144 }
2145 }
2146 } else {
2147 for (const connectorId of this.connectors.keys()) {
2148 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2149 await this.stopTransactionOnConnector(connectorId, reason)
2150 }
2151 }
2152 }
2153 }
2154
2155 // 0 for disabling
2156 private getConnectionTimeout (): number {
2157 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
2158 return convertToInt(
2159 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
2160 Constants.DEFAULT_CONNECTION_TIMEOUT
2161 )
2162 }
2163 return Constants.DEFAULT_CONNECTION_TIMEOUT
2164 }
2165
2166 private getPowerDivider (): number {
2167 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
2168 if (this.stationInfo?.powerSharedByConnectors === true) {
2169 powerDivider = this.getNumberOfRunningTransactions()
2170 }
2171 return powerDivider
2172 }
2173
2174 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2175 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2176 const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
2177 switch (this.getCurrentOutType(stationInfo)) {
2178 case CurrentType.AC:
2179 return ACElectricUtils.amperagePerPhaseFromPower(
2180 this.getNumberOfPhases(stationInfo),
2181 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2182 this.getVoltageOut(stationInfo)
2183 )
2184 case CurrentType.DC:
2185 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
2186 }
2187 }
2188
2189 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
2190 return (
2191 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2192 (stationInfo ?? this.stationInfo!).currentOutType ??
2193 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2194 Constants.DEFAULT_STATION_INFO.currentOutType!
2195 )
2196 }
2197
2198 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
2199 return (
2200 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2201 (stationInfo ?? this.stationInfo!).voltageOut ??
2202 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
2203 )
2204 }
2205
2206 private getAmperageLimitation (): number | undefined {
2207 if (
2208 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2209 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
2210 ) {
2211 return (
2212 convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
2213 getAmperageLimitationUnitDivider(this.stationInfo)
2214 )
2215 }
2216 }
2217
2218 private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
2219 if (this.stationInfo?.autoRegister === true) {
2220 await this.ocppRequestService.requestHandler<
2221 BootNotificationRequest,
2222 BootNotificationResponse
2223 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2224 skipBufferingOnError: true
2225 })
2226 }
2227 // Start WebSocket ping
2228 if (this.wsPingSetInterval == null) {
2229 this.startWebSocketPing()
2230 }
2231 // Start heartbeat
2232 if (this.heartbeatSetInterval == null) {
2233 this.startHeartbeat()
2234 }
2235 // Initialize connectors status
2236 if (this.hasEvses) {
2237 for (const [evseId, evseStatus] of this.evses) {
2238 if (evseId > 0) {
2239 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2240 await sendAndSetConnectorStatus(
2241 this,
2242 connectorId,
2243 getBootConnectorStatus(this, connectorId, connectorStatus),
2244 evseId
2245 )
2246 }
2247 }
2248 }
2249 } else {
2250 for (const connectorId of this.connectors.keys()) {
2251 if (connectorId > 0) {
2252 await sendAndSetConnectorStatus(
2253 this,
2254 connectorId,
2255 getBootConnectorStatus(
2256 this,
2257 connectorId,
2258 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2259 this.getConnectorStatus(connectorId)!
2260 )
2261 )
2262 }
2263 }
2264 }
2265 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2266 await this.ocppRequestService.requestHandler<
2267 FirmwareStatusNotificationRequest,
2268 FirmwareStatusNotificationResponse
2269 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2270 status: FirmwareStatus.Installed
2271 })
2272 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
2273 }
2274
2275 // Start the ATG
2276 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2277 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
2278 }
2279 this.flushMessageBuffer()
2280 }
2281
2282 private internalStopMessageSequence (): void {
2283 // Stop WebSocket ping
2284 this.stopWebSocketPing()
2285 // Stop heartbeat
2286 this.stopHeartbeat()
2287 // Stop the ATG
2288 if (this.automaticTransactionGenerator?.started === true) {
2289 this.stopAutomaticTransactionGenerator()
2290 }
2291 }
2292
2293 private async stopMessageSequence (
2294 reason?: StopTransactionReason,
2295 stopTransactions?: boolean
2296 ): Promise<void> {
2297 this.internalStopMessageSequence()
2298 // Stop ongoing transactions
2299 stopTransactions === true && (await this.stopRunningTransactions(reason))
2300 if (this.hasEvses) {
2301 for (const [evseId, evseStatus] of this.evses) {
2302 if (evseId > 0) {
2303 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2304 await sendAndSetConnectorStatus(
2305 this,
2306 connectorId,
2307 ConnectorStatusEnum.Unavailable,
2308 evseId
2309 )
2310 delete connectorStatus.status
2311 }
2312 }
2313 }
2314 } else {
2315 for (const connectorId of this.connectors.keys()) {
2316 if (connectorId > 0) {
2317 await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
2318 delete this.getConnectorStatus(connectorId)?.status
2319 }
2320 }
2321 }
2322 }
2323
2324 private getWebSocketPingInterval (): number {
2325 return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
2326 ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
2327 : 0
2328 }
2329
2330 private startWebSocketPing (): void {
2331 const webSocketPingInterval = this.getWebSocketPingInterval()
2332 if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
2333 this.wsPingSetInterval = setInterval(() => {
2334 if (this.isWebSocketConnectionOpened()) {
2335 this.wsConnection?.ping()
2336 }
2337 }, secondsToMilliseconds(webSocketPingInterval))
2338 logger.info(
2339 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2340 webSocketPingInterval
2341 )}`
2342 )
2343 } else if (this.wsPingSetInterval != null) {
2344 logger.info(
2345 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2346 webSocketPingInterval
2347 )}`
2348 )
2349 } else {
2350 logger.error(
2351 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2352 )
2353 }
2354 }
2355
2356 private stopWebSocketPing (): void {
2357 if (this.wsPingSetInterval != null) {
2358 clearInterval(this.wsPingSetInterval)
2359 delete this.wsPingSetInterval
2360 }
2361 }
2362
2363 private getConfiguredSupervisionUrl (): URL {
2364 let configuredSupervisionUrl: string
2365 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
2366 if (isNotEmptyArray(supervisionUrls)) {
2367 let configuredSupervisionUrlIndex: number
2368 switch (Configuration.getSupervisionUrlDistribution()) {
2369 case SupervisionUrlDistribution.RANDOM:
2370 configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
2371 break
2372 case SupervisionUrlDistribution.ROUND_ROBIN:
2373 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2374 default:
2375 !Object.values(SupervisionUrlDistribution).includes(
2376 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2377 Configuration.getSupervisionUrlDistribution()!
2378 ) &&
2379 logger.warn(
2380 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2381 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2382 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2383 }'`
2384 )
2385 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
2386 break
2387 }
2388 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
2389 } else {
2390 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2391 configuredSupervisionUrl = supervisionUrls!
2392 }
2393 if (isNotEmptyString(configuredSupervisionUrl)) {
2394 return new URL(configuredSupervisionUrl)
2395 }
2396 const errorMsg = 'No supervision url(s) configured'
2397 logger.error(`${this.logPrefix()} ${errorMsg}`)
2398 throw new BaseError(errorMsg)
2399 }
2400
2401 private stopHeartbeat (): void {
2402 if (this.heartbeatSetInterval != null) {
2403 clearInterval(this.heartbeatSetInterval)
2404 delete this.heartbeatSetInterval
2405 }
2406 }
2407
2408 private terminateWSConnection (): void {
2409 if (this.isWebSocketConnectionOpened()) {
2410 this.wsConnection?.terminate()
2411 this.wsConnection = null
2412 }
2413 }
2414
2415 private async reconnect (): Promise<void> {
2416 if (
2417 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2418 this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
2419 this.stationInfo?.autoReconnectMaxRetries === -1
2420 ) {
2421 ++this.wsConnectionRetryCount
2422 const reconnectDelay =
2423 this.stationInfo?.reconnectExponentialDelay === true
2424 ? exponentialDelay(this.wsConnectionRetryCount)
2425 : secondsToMilliseconds(this.getConnectionTimeout())
2426 const reconnectDelayWithdraw = 1000
2427 const reconnectTimeout =
2428 reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
2429 logger.error(
2430 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2431 reconnectDelay,
2432 2
2433 )}ms, timeout ${reconnectTimeout}ms`
2434 )
2435 await sleep(reconnectDelay)
2436 logger.error(
2437 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2438 )
2439 this.openWSConnection(
2440 {
2441 handshakeTimeout: reconnectTimeout
2442 },
2443 { closeOpened: true }
2444 )
2445 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
2446 logger.error(
2447 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`
2448 )
2449 }
2450 }
2451}