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