build(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
2
3 import { createHash, randomInt } from 'node:crypto'
4 import { EventEmitter } from 'node:events'
5 import { existsSync, type FSWatcher, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
6 import { dirname, join } from 'node:path'
7 import { URL } from 'node:url'
8 import { parentPort } from 'node:worker_threads'
9
10 import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns'
11 import { mergeDeepRight, once } from 'rambda'
12 import { type RawData, WebSocket } from 'ws'
13
14 import { BaseError, OCPPError } from '../exception/index.js'
15 import { PerformanceStatistics } from '../performance/index.js'
16 import {
17 type AutomaticTransactionGeneratorConfiguration,
18 AvailabilityType,
19 type BootNotificationRequest,
20 type BootNotificationResponse,
21 type CachedRequest,
22 type ChargingStationConfiguration,
23 ChargingStationEvents,
24 type ChargingStationInfo,
25 type ChargingStationOcppConfiguration,
26 type ChargingStationOptions,
27 type ChargingStationTemplate,
28 type ConnectorStatus,
29 ConnectorStatusEnum,
30 CurrentType,
31 type ErrorCallback,
32 type ErrorResponse,
33 ErrorType,
34 type EvseStatus,
35 type EvseStatusConfiguration,
36 FileType,
37 FirmwareStatus,
38 type FirmwareStatusNotificationRequest,
39 type FirmwareStatusNotificationResponse,
40 type HeartbeatRequest,
41 type HeartbeatResponse,
42 type IncomingRequest,
43 type IncomingRequestCommand,
44 MessageType,
45 MeterValueMeasurand,
46 type MeterValuesRequest,
47 type MeterValuesResponse,
48 OCPPVersion,
49 type OutgoingRequest,
50 PowerUnits,
51 RegistrationStatusEnumType,
52 RequestCommand,
53 type Reservation,
54 type ReservationKey,
55 ReservationTerminationReason,
56 type Response,
57 StandardParametersKey,
58 type Status,
59 type StopTransactionReason,
60 type StopTransactionRequest,
61 type StopTransactionResponse,
62 SupervisionUrlDistribution,
63 SupportedFeatureProfiles,
64 type Voltage,
65 WebSocketCloseEventStatusCode,
66 type WSError,
67 type WsOptions
68 } from '../types/index.js'
69 import {
70 ACElectricUtils,
71 AsyncLock,
72 AsyncLockType,
73 buildAddedMessage,
74 buildChargingStationAutomaticTransactionGeneratorConfiguration,
75 buildConnectorsStatus,
76 buildDeletedMessage,
77 buildEvsesStatus,
78 buildStartedMessage,
79 buildStoppedMessage,
80 buildUpdatedMessage,
81 clone,
82 Configuration,
83 Constants,
84 convertToBoolean,
85 convertToDate,
86 convertToInt,
87 DCElectricUtils,
88 exponentialDelay,
89 formatDurationMilliSeconds,
90 formatDurationSeconds,
91 getWebSocketCloseEventStatusString,
92 handleFileException,
93 isNotEmptyArray,
94 isNotEmptyString,
95 logger,
96 logPrefix,
97 min,
98 roundTo,
99 secureRandom,
100 sleep,
101 watchJsonFile
102 } from '../utils/index.js'
103 import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js'
104 import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel.js'
105 import {
106 addConfigurationKey,
107 deleteConfigurationKey,
108 getConfigurationKey,
109 setConfigurationKeyValue
110 } from './ConfigurationKeyUtils.js'
111 import {
112 buildConnectorsMap,
113 buildTemplateName,
114 checkChargingStation,
115 checkConfiguration,
116 checkConnectorsConfiguration,
117 checkStationInfoConnectorStatus,
118 checkTemplate,
119 createBootNotificationRequest,
120 createSerialNumber,
121 getAmperageLimitationUnitDivider,
122 getBootConnectorStatus,
123 getChargingStationConnectorChargingProfilesPowerLimit,
124 getChargingStationId,
125 getDefaultVoltageOut,
126 getHashId,
127 getIdTagsFile,
128 getMaxNumberOfEvses,
129 getNumberOfReservableConnectors,
130 getPhaseRotationValue,
131 hasFeatureProfile,
132 hasReservationExpired,
133 initializeConnectorsMapStatus,
134 propagateSerialNumber,
135 setChargingStationOptions,
136 stationTemplateToStationInfo,
137 warnTemplateKeysDeprecation
138 } from './Helpers.js'
139 import { IdTagsCache } from './IdTagsCache.js'
140 import {
141 buildMeterValue,
142 buildTransactionEndMeterValue,
143 getMessageTypeString,
144 OCPP16IncomingRequestService,
145 OCPP16RequestService,
146 OCPP16ResponseService,
147 OCPP20IncomingRequestService,
148 OCPP20RequestService,
149 OCPP20ResponseService,
150 type OCPPIncomingRequestService,
151 type OCPPRequestService,
152 sendAndSetConnectorStatus
153 } from './ocpp/index.js'
154 import { SharedLRUCache } from './SharedLRUCache.js'
155
156 export class ChargingStation extends EventEmitter {
157 public readonly index: number
158 public readonly templateFile: string
159 public stationInfo?: ChargingStationInfo
160 public started: boolean
161 public starting: boolean
162 public idTagsCache: IdTagsCache
163 public automaticTransactionGenerator?: AutomaticTransactionGenerator
164 public ocppConfiguration?: ChargingStationOcppConfiguration
165 public wsConnection: WebSocket | null
166 public readonly connectors: Map<number, ConnectorStatus>
167 public readonly evses: Map<number, EvseStatus>
168 public readonly requests: Map<string, CachedRequest>
169 public performanceStatistics?: PerformanceStatistics
170 public heartbeatSetInterval?: NodeJS.Timeout
171 public ocppRequestService!: OCPPRequestService
172 public bootNotificationRequest?: BootNotificationRequest
173 public bootNotificationResponse?: BootNotificationResponse
174 public powerDivider?: number
175 private stopping: boolean
176 private configurationFile!: string
177 private configurationFileHash!: string
178 private connectorsConfigurationHash!: string
179 private evsesConfigurationHash!: string
180 private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
181 private ocppIncomingRequestService!: OCPPIncomingRequestService
182 private readonly messageBuffer: Set<string>
183 private configuredSupervisionUrl!: URL
184 private wsConnectionRetried: boolean
185 private wsConnectionRetryCount: number
186 private templateFileWatcher?: FSWatcher
187 private templateFileHash!: string
188 private readonly sharedLRUCache: SharedLRUCache
189 private wsPingSetInterval?: NodeJS.Timeout
190 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
191 private flushMessageBufferSetInterval?: NodeJS.Timeout
192
193 constructor (index: number, templateFile: string, options?: ChargingStationOptions) {
194 super()
195 this.started = false
196 this.starting = false
197 this.stopping = false
198 this.wsConnection = null
199 this.wsConnectionRetried = false
200 this.wsConnectionRetryCount = 0
201 this.index = index
202 this.templateFile = templateFile
203 this.connectors = new Map<number, ConnectorStatus>()
204 this.evses = new Map<number, EvseStatus>()
205 this.requests = new Map<string, CachedRequest>()
206 this.messageBuffer = new Set<string>()
207 this.sharedLRUCache = SharedLRUCache.getInstance()
208 this.idTagsCache = IdTagsCache.getInstance()
209 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this)
210
211 this.on(ChargingStationEvents.added, () => {
212 parentPort?.postMessage(buildAddedMessage(this))
213 })
214 this.on(ChargingStationEvents.deleted, () => {
215 parentPort?.postMessage(buildDeletedMessage(this))
216 })
217 this.on(ChargingStationEvents.started, () => {
218 parentPort?.postMessage(buildStartedMessage(this))
219 })
220 this.on(ChargingStationEvents.stopped, () => {
221 parentPort?.postMessage(buildStoppedMessage(this))
222 })
223 this.on(ChargingStationEvents.updated, () => {
224 parentPort?.postMessage(buildUpdatedMessage(this))
225 })
226 this.on(ChargingStationEvents.accepted, () => {
227 this.startMessageSequence(
228 this.wsConnectionRetried
229 ? true
230 : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
231 ).catch((error: unknown) => {
232 logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
233 })
234 this.wsConnectionRetried = false
235 })
236 this.on(ChargingStationEvents.rejected, () => {
237 this.wsConnectionRetried = false
238 })
239 this.on(ChargingStationEvents.disconnected, () => {
240 try {
241 this.internalStopMessageSequence()
242 } catch (error) {
243 logger.error(
244 `${this.logPrefix()} Error while stopping the internal message sequence:`,
245 error
246 )
247 }
248 })
249
250 this.initialize(options)
251
252 this.add()
253
254 if (this.stationInfo?.autoStart === true) {
255 this.start()
256 }
257 }
258
259 public get hasEvses (): boolean {
260 return this.connectors.size === 0 && this.evses.size > 0
261 }
262
263 public get wsConnectionUrl (): URL {
264 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 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1836 BootNotificationRequest,
1837 BootNotificationResponse
1838 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1839 skipBufferingOnError: true
1840 })
1841 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1842 if (this.bootNotificationResponse?.currentTime != null) {
1843 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1844 this.bootNotificationResponse.currentTime = convertToDate(
1845 this.bootNotificationResponse.currentTime
1846 )!
1847 }
1848 if (!this.isRegistered()) {
1849 this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
1850 await sleep(
1851 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1852 this.bootNotificationResponse?.interval != null
1853 ? secondsToMilliseconds(this.bootNotificationResponse.interval)
1854 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1855 )
1856 }
1857 } while (
1858 !this.isRegistered() &&
1859 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1860 (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
1861 this.stationInfo?.registrationMaxRetries === -1)
1862 )
1863 }
1864 if (this.isRegistered()) {
1865 this.emit(ChargingStationEvents.registered)
1866 if (this.inAcceptedState()) {
1867 this.emit(ChargingStationEvents.accepted)
1868 }
1869 } else {
1870 if (this.inRejectedState()) {
1871 this.emit(ChargingStationEvents.rejected)
1872 }
1873 logger.error(
1874 `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
1875 this.stationInfo?.registrationMaxRetries
1876 })`
1877 )
1878 }
1879 this.wsConnectionRetryCount = 0
1880 this.emit(ChargingStationEvents.updated)
1881 } else {
1882 logger.warn(
1883 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
1884 )
1885 }
1886 }
1887
1888 private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
1889 this.emit(ChargingStationEvents.disconnected)
1890 this.emit(ChargingStationEvents.updated)
1891 switch (code) {
1892 // Normal close
1893 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1894 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1895 logger.info(
1896 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1897 code
1898 )}' and reason '${reason.toString()}'`
1899 )
1900 this.wsConnectionRetryCount = 0
1901 break
1902 // Abnormal close
1903 default:
1904 logger.error(
1905 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1906 code
1907 )}' and reason '${reason.toString()}'`
1908 )
1909 this.started &&
1910 this.reconnect()
1911 .then(() => {
1912 this.emit(ChargingStationEvents.updated)
1913 })
1914 .catch((error: unknown) =>
1915 logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
1916 )
1917 break
1918 }
1919 }
1920
1921 private getCachedRequest (
1922 messageType: MessageType | undefined,
1923 messageId: string
1924 ): CachedRequest | undefined {
1925 const cachedRequest = this.requests.get(messageId)
1926 if (Array.isArray(cachedRequest)) {
1927 return cachedRequest
1928 }
1929 throw new OCPPError(
1930 ErrorType.PROTOCOL_ERROR,
1931 `Cached request for message id ${messageId} ${getMessageTypeString(
1932 messageType
1933 )} is not an array`,
1934 undefined,
1935 cachedRequest
1936 )
1937 }
1938
1939 private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
1940 const [messageType, messageId, commandName, commandPayload] = request
1941 if (this.stationInfo?.enableStatistics === true) {
1942 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
1943 }
1944 logger.debug(
1945 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1946 request
1947 )}`
1948 )
1949 // Process the message
1950 await this.ocppIncomingRequestService.incomingRequestHandler(
1951 this,
1952 messageId,
1953 commandName,
1954 commandPayload
1955 )
1956 this.emit(ChargingStationEvents.updated)
1957 }
1958
1959 private handleResponseMessage (response: Response): void {
1960 const [messageType, messageId, commandPayload] = response
1961 if (!this.requests.has(messageId)) {
1962 // Error
1963 throw new OCPPError(
1964 ErrorType.INTERNAL_ERROR,
1965 `Response for unknown message id ${messageId}`,
1966 undefined,
1967 commandPayload
1968 )
1969 }
1970 // Respond
1971 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1972 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1973 messageType,
1974 messageId
1975 )!
1976 logger.debug(
1977 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1978 response
1979 )}`
1980 )
1981 responseCallback(commandPayload, requestPayload)
1982 }
1983
1984 private handleErrorMessage (errorResponse: ErrorResponse): void {
1985 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1986 if (!this.requests.has(messageId)) {
1987 // Error
1988 throw new OCPPError(
1989 ErrorType.INTERNAL_ERROR,
1990 `Error response for unknown message id ${messageId}`,
1991 undefined,
1992 { errorType, errorMessage, errorDetails }
1993 )
1994 }
1995 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1996 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1997 logger.debug(
1998 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1999 errorResponse
2000 )}`
2001 )
2002 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
2003 }
2004
2005 private async onMessage (data: RawData): Promise<void> {
2006 let request: IncomingRequest | Response | ErrorResponse | undefined
2007 let messageType: MessageType | undefined
2008 let errorMsg: string
2009 try {
2010 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2011 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
2012 if (Array.isArray(request)) {
2013 [messageType] = request
2014 // Check the type of message
2015 switch (messageType) {
2016 // Incoming Message
2017 case MessageType.CALL_MESSAGE:
2018 await this.handleIncomingMessage(request as IncomingRequest)
2019 break
2020 // Response Message
2021 case MessageType.CALL_RESULT_MESSAGE:
2022 this.handleResponseMessage(request as Response)
2023 break
2024 // Error Message
2025 case MessageType.CALL_ERROR_MESSAGE:
2026 this.handleErrorMessage(request as ErrorResponse)
2027 break
2028 // Unknown Message
2029 default:
2030 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2031 errorMsg = `Wrong message type ${messageType}`
2032 logger.error(`${this.logPrefix()} ${errorMsg}`)
2033 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
2034 }
2035 } else {
2036 throw new OCPPError(
2037 ErrorType.PROTOCOL_ERROR,
2038 'Incoming message is not an array',
2039 undefined,
2040 {
2041 request
2042 }
2043 )
2044 }
2045 } catch (error) {
2046 if (!Array.isArray(request)) {
2047 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
2048 return
2049 }
2050 let commandName: IncomingRequestCommand | undefined
2051 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
2052 let errorCallback: ErrorCallback
2053 const [, messageId] = request
2054 switch (messageType) {
2055 case MessageType.CALL_MESSAGE:
2056 [, , commandName] = request as IncomingRequest
2057 // Send error
2058 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
2059 break
2060 case MessageType.CALL_RESULT_MESSAGE:
2061 case MessageType.CALL_ERROR_MESSAGE:
2062 if (this.requests.has(messageId)) {
2063 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2064 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
2065 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2066 errorCallback(error as OCPPError, false)
2067 } else {
2068 // Remove the request from the cache in case of error at response handling
2069 this.requests.delete(messageId)
2070 }
2071 break
2072 }
2073 if (!(error instanceof OCPPError)) {
2074 logger.warn(
2075 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2076 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2077 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2078 }' message '${data.toString()}' handling is not an OCPPError:`,
2079 error
2080 )
2081 }
2082 logger.error(
2083 `${this.logPrefix()} Incoming OCPP command '${
2084 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2085 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2086 }' message '${data.toString()}'${
2087 this.requests.has(messageId)
2088 ? ` matching cached request '${JSON.stringify(
2089 this.getCachedRequest(messageType, messageId)
2090 )}'`
2091 : ''
2092 } processing error:`,
2093 error
2094 )
2095 }
2096 }
2097
2098 private onPing (): void {
2099 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2100 }
2101
2102 private onPong (): void {
2103 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2104 }
2105
2106 private onError (error: WSError): void {
2107 this.closeWSConnection()
2108 logger.error(`${this.logPrefix()} WebSocket error:`, error)
2109 }
2110
2111 private getEnergyActiveImportRegister (
2112 connectorStatus: ConnectorStatus | undefined,
2113 rounded = false
2114 ): number {
2115 if (this.stationInfo?.meteringPerTransaction === true) {
2116 return (
2117 (rounded
2118 ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
2119 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
2120 : undefined
2121 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2122 )
2123 }
2124 return (
2125 (rounded
2126 ? connectorStatus?.energyActiveImportRegisterValue != null
2127 ? Math.round(connectorStatus.energyActiveImportRegisterValue)
2128 : undefined
2129 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2130 )
2131 }
2132
2133 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2134 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2135 return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
2136 }
2137
2138 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
2139 if (this.hasEvses) {
2140 for (const [evseId, evseStatus] of this.evses) {
2141 if (evseId === 0) {
2142 continue
2143 }
2144 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2145 if (connectorStatus.transactionStarted === true) {
2146 await this.stopTransactionOnConnector(connectorId, reason)
2147 }
2148 }
2149 }
2150 } else {
2151 for (const connectorId of this.connectors.keys()) {
2152 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2153 await this.stopTransactionOnConnector(connectorId, reason)
2154 }
2155 }
2156 }
2157 }
2158
2159 // 0 for disabling
2160 private getConnectionTimeout (): number {
2161 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
2162 return convertToInt(
2163 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
2164 Constants.DEFAULT_CONNECTION_TIMEOUT
2165 )
2166 }
2167 return Constants.DEFAULT_CONNECTION_TIMEOUT
2168 }
2169
2170 private getPowerDivider (): number {
2171 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
2172 if (this.stationInfo?.powerSharedByConnectors === true) {
2173 powerDivider = this.getNumberOfRunningTransactions()
2174 }
2175 return powerDivider
2176 }
2177
2178 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2179 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2180 const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
2181 switch (this.getCurrentOutType(stationInfo)) {
2182 case CurrentType.AC:
2183 return ACElectricUtils.amperagePerPhaseFromPower(
2184 this.getNumberOfPhases(stationInfo),
2185 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2186 this.getVoltageOut(stationInfo)
2187 )
2188 case CurrentType.DC:
2189 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
2190 }
2191 }
2192
2193 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
2194 return (
2195 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2196 (stationInfo ?? this.stationInfo!).currentOutType ??
2197 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2198 Constants.DEFAULT_STATION_INFO.currentOutType!
2199 )
2200 }
2201
2202 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
2203 return (
2204 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2205 (stationInfo ?? this.stationInfo!).voltageOut ??
2206 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
2207 )
2208 }
2209
2210 private getAmperageLimitation (): number | undefined {
2211 if (
2212 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2213 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
2214 ) {
2215 return (
2216 convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
2217 getAmperageLimitationUnitDivider(this.stationInfo)
2218 )
2219 }
2220 }
2221
2222 private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
2223 if (this.stationInfo?.autoRegister === true) {
2224 await this.ocppRequestService.requestHandler<
2225 BootNotificationRequest,
2226 BootNotificationResponse
2227 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2228 skipBufferingOnError: true
2229 })
2230 }
2231 // Start WebSocket ping
2232 this.startWebSocketPing()
2233 // Start heartbeat
2234 this.startHeartbeat()
2235 // Initialize connectors status
2236 if (this.hasEvses) {
2237 for (const [evseId, evseStatus] of this.evses) {
2238 if (evseId > 0) {
2239 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2240 await sendAndSetConnectorStatus(
2241 this,
2242 connectorId,
2243 getBootConnectorStatus(this, connectorId, connectorStatus),
2244 evseId
2245 )
2246 }
2247 }
2248 }
2249 } else {
2250 for (const connectorId of this.connectors.keys()) {
2251 if (connectorId > 0) {
2252 await sendAndSetConnectorStatus(
2253 this,
2254 connectorId,
2255 getBootConnectorStatus(
2256 this,
2257 connectorId,
2258 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2259 this.getConnectorStatus(connectorId)!
2260 )
2261 )
2262 }
2263 }
2264 }
2265 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2266 await this.ocppRequestService.requestHandler<
2267 FirmwareStatusNotificationRequest,
2268 FirmwareStatusNotificationResponse
2269 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2270 status: FirmwareStatus.Installed
2271 })
2272 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
2273 }
2274
2275 // Start the ATG
2276 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2277 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
2278 }
2279 this.flushMessageBuffer()
2280 }
2281
2282 private internalStopMessageSequence (): void {
2283 // Stop WebSocket ping
2284 this.stopWebSocketPing()
2285 // Stop heartbeat
2286 this.stopHeartbeat()
2287 // Stop the ATG
2288 if (this.automaticTransactionGenerator?.started === true) {
2289 this.stopAutomaticTransactionGenerator()
2290 }
2291 }
2292
2293 private async stopMessageSequence (
2294 reason?: StopTransactionReason,
2295 stopTransactions?: boolean
2296 ): Promise<void> {
2297 this.internalStopMessageSequence()
2298 // Stop ongoing transactions
2299 stopTransactions === true && (await this.stopRunningTransactions(reason))
2300 if (this.hasEvses) {
2301 for (const [evseId, evseStatus] of this.evses) {
2302 if (evseId > 0) {
2303 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2304 await sendAndSetConnectorStatus(
2305 this,
2306 connectorId,
2307 ConnectorStatusEnum.Unavailable,
2308 evseId
2309 )
2310 delete connectorStatus.status
2311 }
2312 }
2313 }
2314 } else {
2315 for (const connectorId of this.connectors.keys()) {
2316 if (connectorId > 0) {
2317 await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
2318 delete this.getConnectorStatus(connectorId)?.status
2319 }
2320 }
2321 }
2322 }
2323
2324 private startWebSocketPing (): void {
2325 const webSocketPingInterval =
2326 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
2327 ? convertToInt(
2328 getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
2329 )
2330 : 0
2331 if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
2332 this.wsPingSetInterval = setInterval(() => {
2333 if (this.isWebSocketConnectionOpened()) {
2334 this.wsConnection?.ping()
2335 }
2336 }, secondsToMilliseconds(webSocketPingInterval))
2337 logger.info(
2338 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2339 webSocketPingInterval
2340 )}`
2341 )
2342 } else if (this.wsPingSetInterval != null) {
2343 logger.info(
2344 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2345 webSocketPingInterval
2346 )}`
2347 )
2348 } else {
2349 logger.error(
2350 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2351 )
2352 }
2353 }
2354
2355 private stopWebSocketPing (): void {
2356 if (this.wsPingSetInterval != null) {
2357 clearInterval(this.wsPingSetInterval)
2358 delete this.wsPingSetInterval
2359 }
2360 }
2361
2362 private getConfiguredSupervisionUrl (): URL {
2363 let configuredSupervisionUrl: string
2364 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
2365 if (isNotEmptyArray(supervisionUrls)) {
2366 let configuredSupervisionUrlIndex: number
2367 switch (Configuration.getSupervisionUrlDistribution()) {
2368 case SupervisionUrlDistribution.RANDOM:
2369 configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
2370 break
2371 case SupervisionUrlDistribution.ROUND_ROBIN:
2372 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2373 default:
2374 !Object.values(SupervisionUrlDistribution).includes(
2375 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2376 Configuration.getSupervisionUrlDistribution()!
2377 ) &&
2378 logger.warn(
2379 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2380 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2381 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2382 }'`
2383 )
2384 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
2385 break
2386 }
2387 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
2388 } else {
2389 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2390 configuredSupervisionUrl = supervisionUrls!
2391 }
2392 if (isNotEmptyString(configuredSupervisionUrl)) {
2393 return new URL(configuredSupervisionUrl)
2394 }
2395 const errorMsg = 'No supervision url(s) configured'
2396 logger.error(`${this.logPrefix()} ${errorMsg}`)
2397 throw new BaseError(errorMsg)
2398 }
2399
2400 private stopHeartbeat (): void {
2401 if (this.heartbeatSetInterval != null) {
2402 clearInterval(this.heartbeatSetInterval)
2403 delete this.heartbeatSetInterval
2404 }
2405 }
2406
2407 private terminateWSConnection (): void {
2408 if (this.isWebSocketConnectionOpened()) {
2409 this.wsConnection?.terminate()
2410 this.wsConnection = null
2411 }
2412 }
2413
2414 private async reconnect (): Promise<void> {
2415 if (
2416 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2417 this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
2418 this.stationInfo?.autoReconnectMaxRetries === -1
2419 ) {
2420 this.wsConnectionRetried = true
2421 ++this.wsConnectionRetryCount
2422 const reconnectDelay =
2423 this.stationInfo?.reconnectExponentialDelay === true
2424 ? exponentialDelay(this.wsConnectionRetryCount)
2425 : secondsToMilliseconds(this.getConnectionTimeout())
2426 const reconnectDelayWithdraw = 1000
2427 const reconnectTimeout =
2428 reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
2429 logger.error(
2430 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2431 reconnectDelay,
2432 2
2433 )}ms, timeout ${reconnectTimeout}ms`
2434 )
2435 await sleep(reconnectDelay)
2436 logger.error(
2437 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2438 )
2439 this.openWSConnection(
2440 {
2441 handshakeTimeout: reconnectTimeout
2442 },
2443 { closeOpened: true }
2444 )
2445 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
2446 logger.error(
2447 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2448 this.wsConnectionRetryCount
2449 }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
2450 )
2451 }
2452 }
2453 }