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