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