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