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