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