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