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