fix: fix date handling connectorsStatus configuration file section
[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.stationInfo?.enableStatistics === true) {
1939 this.performanceStatistics?.addRequestStatistic(commandName, messageType)
1940 }
1941 logger.debug(
1942 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1943 request
1944 )}`
1945 )
1946 // Process the message
1947 await this.ocppIncomingRequestService.incomingRequestHandler(
1948 this,
1949 messageId,
1950 commandName,
1951 commandPayload
1952 )
1953 this.emit(ChargingStationEvents.updated)
1954 }
1955
1956 private handleResponseMessage (response: Response): void {
1957 const [messageType, messageId, commandPayload] = response
1958 if (!this.requests.has(messageId)) {
1959 // Error
1960 throw new OCPPError(
1961 ErrorType.INTERNAL_ERROR,
1962 `Response for unknown message id ${messageId}`,
1963 undefined,
1964 commandPayload
1965 )
1966 }
1967 // Respond
1968 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1969 const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
1970 messageType,
1971 messageId
1972 )!
1973 logger.debug(
1974 `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
1975 response
1976 )}`
1977 )
1978 responseCallback(commandPayload, requestPayload)
1979 }
1980
1981 private handleErrorMessage (errorResponse: ErrorResponse): void {
1982 const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
1983 if (!this.requests.has(messageId)) {
1984 // Error
1985 throw new OCPPError(
1986 ErrorType.INTERNAL_ERROR,
1987 `Error response for unknown message id ${messageId}`,
1988 undefined,
1989 { errorType, errorMessage, errorDetails }
1990 )
1991 }
1992 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1993 const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
1994 logger.debug(
1995 `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
1996 errorResponse
1997 )}`
1998 )
1999 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
2000 }
2001
2002 private async onMessage (data: RawData): Promise<void> {
2003 let request: IncomingRequest | Response | ErrorResponse | undefined
2004 let messageType: MessageType | undefined
2005 let errorMsg: string
2006 try {
2007 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2008 request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
2009 if (Array.isArray(request)) {
2010 [messageType] = request
2011 // Check the type of message
2012 switch (messageType) {
2013 // Incoming Message
2014 case MessageType.CALL_MESSAGE:
2015 await this.handleIncomingMessage(request as IncomingRequest)
2016 break
2017 // Response Message
2018 case MessageType.CALL_RESULT_MESSAGE:
2019 this.handleResponseMessage(request as Response)
2020 break
2021 // Error Message
2022 case MessageType.CALL_ERROR_MESSAGE:
2023 this.handleErrorMessage(request as ErrorResponse)
2024 break
2025 // Unknown Message
2026 default:
2027 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2028 errorMsg = `Wrong message type ${messageType}`
2029 logger.error(`${this.logPrefix()} ${errorMsg}`)
2030 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
2031 }
2032 } else {
2033 throw new OCPPError(
2034 ErrorType.PROTOCOL_ERROR,
2035 'Incoming message is not an array',
2036 undefined,
2037 {
2038 request
2039 }
2040 )
2041 }
2042 } catch (error) {
2043 if (!Array.isArray(request)) {
2044 logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
2045 return
2046 }
2047 let commandName: IncomingRequestCommand | undefined
2048 let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
2049 let errorCallback: ErrorCallback
2050 const [, messageId] = request
2051 switch (messageType) {
2052 case MessageType.CALL_MESSAGE:
2053 [, , commandName] = request as IncomingRequest
2054 // Send error
2055 await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
2056 break
2057 case MessageType.CALL_RESULT_MESSAGE:
2058 case MessageType.CALL_ERROR_MESSAGE:
2059 if (this.requests.has(messageId)) {
2060 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2061 [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
2062 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2063 errorCallback(error as OCPPError, false)
2064 } else {
2065 // Remove the request from the cache in case of error at response handling
2066 this.requests.delete(messageId)
2067 }
2068 break
2069 }
2070 if (!(error instanceof OCPPError)) {
2071 logger.warn(
2072 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2073 commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
2074 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2075 }' message '${data.toString()}' handling is not an OCPPError:`,
2076 error
2077 )
2078 }
2079 logger.error(
2080 `${this.logPrefix()} 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()}'${
2084 this.requests.has(messageId)
2085 ? ` matching cached request '${JSON.stringify(
2086 this.getCachedRequest(messageType, messageId)
2087 )}'`
2088 : ''
2089 } processing error:`,
2090 error
2091 )
2092 }
2093 }
2094
2095 private onPing (): void {
2096 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
2097 }
2098
2099 private onPong (): void {
2100 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
2101 }
2102
2103 private onError (error: WSError): void {
2104 this.closeWSConnection()
2105 logger.error(`${this.logPrefix()} WebSocket error:`, error)
2106 }
2107
2108 private getEnergyActiveImportRegister (
2109 connectorStatus: ConnectorStatus | undefined,
2110 rounded = false
2111 ): number {
2112 if (this.stationInfo?.meteringPerTransaction === true) {
2113 return (
2114 (rounded
2115 ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
2116 ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
2117 : undefined
2118 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
2119 )
2120 }
2121 return (
2122 (rounded
2123 ? connectorStatus?.energyActiveImportRegisterValue != null
2124 ? Math.round(connectorStatus.energyActiveImportRegisterValue)
2125 : undefined
2126 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
2127 )
2128 }
2129
2130 private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
2131 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2132 return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
2133 }
2134
2135 private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
2136 if (this.hasEvses) {
2137 for (const [evseId, evseStatus] of this.evses) {
2138 if (evseId === 0) {
2139 continue
2140 }
2141 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2142 if (connectorStatus.transactionStarted === true) {
2143 await this.stopTransactionOnConnector(connectorId, reason)
2144 }
2145 }
2146 }
2147 } else {
2148 for (const connectorId of this.connectors.keys()) {
2149 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
2150 await this.stopTransactionOnConnector(connectorId, reason)
2151 }
2152 }
2153 }
2154 }
2155
2156 // 0 for disabling
2157 private getConnectionTimeout (): number {
2158 if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
2159 return convertToInt(
2160 getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
2161 Constants.DEFAULT_CONNECTION_TIMEOUT
2162 )
2163 }
2164 return Constants.DEFAULT_CONNECTION_TIMEOUT
2165 }
2166
2167 private getPowerDivider (): number {
2168 let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
2169 if (this.stationInfo?.powerSharedByConnectors === true) {
2170 powerDivider = this.getNumberOfRunningTransactions()
2171 }
2172 return powerDivider
2173 }
2174
2175 private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
2176 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2177 const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
2178 switch (this.getCurrentOutType(stationInfo)) {
2179 case CurrentType.AC:
2180 return ACElectricUtils.amperagePerPhaseFromPower(
2181 this.getNumberOfPhases(stationInfo),
2182 maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2183 this.getVoltageOut(stationInfo)
2184 )
2185 case CurrentType.DC:
2186 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
2187 }
2188 }
2189
2190 private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
2191 return (
2192 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2193 (stationInfo ?? this.stationInfo!).currentOutType ??
2194 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2195 Constants.DEFAULT_STATION_INFO.currentOutType!
2196 )
2197 }
2198
2199 private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
2200 return (
2201 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2202 (stationInfo ?? this.stationInfo!).voltageOut ??
2203 getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
2204 )
2205 }
2206
2207 private getAmperageLimitation (): number | undefined {
2208 if (
2209 isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
2210 getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
2211 ) {
2212 return (
2213 convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
2214 getAmperageLimitationUnitDivider(this.stationInfo)
2215 )
2216 }
2217 }
2218
2219 private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
2220 if (this.stationInfo?.autoRegister === true) {
2221 await this.ocppRequestService.requestHandler<
2222 BootNotificationRequest,
2223 BootNotificationResponse
2224 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
2225 skipBufferingOnError: true
2226 })
2227 }
2228 // Start WebSocket ping
2229 if (this.wsPingSetInterval == null) {
2230 this.startWebSocketPing()
2231 }
2232 // Start heartbeat
2233 if (this.heartbeatSetInterval == null) {
2234 this.startHeartbeat()
2235 }
2236 // Initialize connectors status
2237 if (this.hasEvses) {
2238 for (const [evseId, evseStatus] of this.evses) {
2239 if (evseId > 0) {
2240 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2241 await sendAndSetConnectorStatus(
2242 this,
2243 connectorId,
2244 getBootConnectorStatus(this, connectorId, connectorStatus),
2245 evseId
2246 )
2247 }
2248 }
2249 }
2250 } else {
2251 for (const connectorId of this.connectors.keys()) {
2252 if (connectorId > 0) {
2253 await sendAndSetConnectorStatus(
2254 this,
2255 connectorId,
2256 getBootConnectorStatus(
2257 this,
2258 connectorId,
2259 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2260 this.getConnectorStatus(connectorId)!
2261 )
2262 )
2263 }
2264 }
2265 }
2266 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
2267 await this.ocppRequestService.requestHandler<
2268 FirmwareStatusNotificationRequest,
2269 FirmwareStatusNotificationResponse
2270 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
2271 status: FirmwareStatus.Installed
2272 })
2273 this.stationInfo.firmwareStatus = FirmwareStatus.Installed
2274 }
2275
2276 // Start the ATG
2277 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
2278 this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
2279 }
2280 this.flushMessageBuffer()
2281 }
2282
2283 private internalStopMessageSequence (): void {
2284 // Stop WebSocket ping
2285 this.stopWebSocketPing()
2286 // Stop heartbeat
2287 this.stopHeartbeat()
2288 // Stop the ATG
2289 if (this.automaticTransactionGenerator?.started === true) {
2290 this.stopAutomaticTransactionGenerator()
2291 }
2292 }
2293
2294 private async stopMessageSequence (
2295 reason?: StopTransactionReason,
2296 stopTransactions?: boolean
2297 ): Promise<void> {
2298 this.internalStopMessageSequence()
2299 // Stop ongoing transactions
2300 stopTransactions === true && (await this.stopRunningTransactions(reason))
2301 if (this.hasEvses) {
2302 for (const [evseId, evseStatus] of this.evses) {
2303 if (evseId > 0) {
2304 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
2305 await sendAndSetConnectorStatus(
2306 this,
2307 connectorId,
2308 ConnectorStatusEnum.Unavailable,
2309 evseId
2310 )
2311 delete connectorStatus.status
2312 }
2313 }
2314 }
2315 } else {
2316 for (const connectorId of this.connectors.keys()) {
2317 if (connectorId > 0) {
2318 await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
2319 delete this.getConnectorStatus(connectorId)?.status
2320 }
2321 }
2322 }
2323 }
2324
2325 private getWebSocketPingInterval (): number {
2326 return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
2327 ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
2328 : 0
2329 }
2330
2331 private startWebSocketPing (): void {
2332 const webSocketPingInterval = this.getWebSocketPingInterval()
2333 if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
2334 this.wsPingSetInterval = setInterval(() => {
2335 if (this.isWebSocketConnectionOpened()) {
2336 this.wsConnection?.ping()
2337 }
2338 }, secondsToMilliseconds(webSocketPingInterval))
2339 logger.info(
2340 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2341 webSocketPingInterval
2342 )}`
2343 )
2344 } else if (this.wsPingSetInterval != null) {
2345 logger.info(
2346 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2347 webSocketPingInterval
2348 )}`
2349 )
2350 } else {
2351 logger.error(
2352 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2353 )
2354 }
2355 }
2356
2357 private stopWebSocketPing (): void {
2358 if (this.wsPingSetInterval != null) {
2359 clearInterval(this.wsPingSetInterval)
2360 delete this.wsPingSetInterval
2361 }
2362 }
2363
2364 private getConfiguredSupervisionUrl (): URL {
2365 let configuredSupervisionUrl: string
2366 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
2367 if (isNotEmptyArray(supervisionUrls)) {
2368 let configuredSupervisionUrlIndex: number
2369 switch (Configuration.getSupervisionUrlDistribution()) {
2370 case SupervisionUrlDistribution.RANDOM:
2371 configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
2372 break
2373 case SupervisionUrlDistribution.ROUND_ROBIN:
2374 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
2375 default:
2376 !Object.values(SupervisionUrlDistribution).includes(
2377 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2378 Configuration.getSupervisionUrlDistribution()!
2379 ) &&
2380 logger.warn(
2381 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2382 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
2383 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2384 }'`
2385 )
2386 configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
2387 break
2388 }
2389 configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
2390 } else {
2391 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2392 configuredSupervisionUrl = supervisionUrls!
2393 }
2394 if (isNotEmptyString(configuredSupervisionUrl)) {
2395 return new URL(configuredSupervisionUrl)
2396 }
2397 const errorMsg = 'No supervision url(s) configured'
2398 logger.error(`${this.logPrefix()} ${errorMsg}`)
2399 throw new BaseError(errorMsg)
2400 }
2401
2402 private stopHeartbeat (): void {
2403 if (this.heartbeatSetInterval != null) {
2404 clearInterval(this.heartbeatSetInterval)
2405 delete this.heartbeatSetInterval
2406 }
2407 }
2408
2409 private terminateWSConnection (): void {
2410 if (this.isWebSocketConnectionOpened()) {
2411 this.wsConnection?.terminate()
2412 this.wsConnection = null
2413 }
2414 }
2415
2416 private async reconnect (): Promise<void> {
2417 if (
2418 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2419 this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
2420 this.stationInfo?.autoReconnectMaxRetries === -1
2421 ) {
2422 ++this.wsConnectionRetryCount
2423 const reconnectDelay =
2424 this.stationInfo?.reconnectExponentialDelay === true
2425 ? exponentialDelay(this.wsConnectionRetryCount)
2426 : secondsToMilliseconds(this.getConnectionTimeout())
2427 const reconnectDelayWithdraw = 1000
2428 const reconnectTimeout =
2429 reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
2430 logger.error(
2431 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2432 reconnectDelay,
2433 2
2434 )}ms, timeout ${reconnectTimeout}ms`
2435 )
2436 await sleep(reconnectDelay)
2437 logger.error(
2438 `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
2439 )
2440 this.openWSConnection(
2441 {
2442 handshakeTimeout: reconnectTimeout
2443 },
2444 { closeOpened: true }
2445 )
2446 } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
2447 logger.error(
2448 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`
2449 )
2450 }
2451 }
2452 }