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