fix: ensure persistence configuration override is taken in all case
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index ba2f30c89f47312fe2f49580b4cc9298d81a806d..9da4fdbb41934219468867ed824b94fd3fb36cd3 100644 (file)
@@ -2,8 +2,8 @@
 
 import { createHash } from 'node:crypto'
 import { EventEmitter } from 'node:events'
-import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
-import { dirname, join } from 'node:path'
+import { type FSWatcher, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { dirname, join, parse } from 'node:path'
 import { URL } from 'node:url'
 import { parentPort } from 'node:worker_threads'
 
@@ -42,6 +42,7 @@ import {
   hasReservationExpired,
   initializeConnectorsMapStatus,
   propagateSerialNumber,
+  setChargingStationOptions,
   stationTemplateToStationInfo,
   warnTemplateKeysDeprecation
 } from './Helpers.js'
@@ -73,6 +74,7 @@ import {
   ChargingStationEvents,
   type ChargingStationInfo,
   type ChargingStationOcppConfiguration,
+  type ChargingStationOptions,
   type ChargingStationTemplate,
   type ConnectorStatus,
   ConnectorStatusEnum,
@@ -123,8 +125,10 @@ import {
   Configuration,
   Constants,
   DCElectricUtils,
+  buildAddedMessage,
   buildChargingStationAutomaticTransactionGeneratorConfiguration,
   buildConnectorsStatus,
+  buildDeletedMessage,
   buildEvsesStatus,
   buildStartedMessage,
   buildStoppedMessage,
@@ -164,7 +168,7 @@ export class ChargingStation extends EventEmitter {
   public readonly connectors: Map<number, ConnectorStatus>
   public readonly evses: Map<number, EvseStatus>
   public readonly requests: Map<string, CachedRequest>
-  public performanceStatistics!: PerformanceStatistics | undefined
+  public performanceStatistics: PerformanceStatistics | undefined
   public heartbeatSetInterval?: NodeJS.Timeout
   public ocppRequestService!: OCPPRequestService
   public bootNotificationRequest?: BootNotificationRequest
@@ -179,21 +183,23 @@ export class ChargingStation extends EventEmitter {
   private ocppIncomingRequestService!: OCPPIncomingRequestService
   private readonly messageBuffer: Set<string>
   private configuredSupervisionUrl!: URL
-  private autoReconnectRetryCount: number
-  private templateFileWatcher!: FSWatcher | undefined
+  private wsConnectionRetried: boolean
+  private wsConnectionRetryCount: number
+  private templateFileWatcher: FSWatcher | undefined
   private templateFileHash!: string
   private readonly sharedLRUCache: SharedLRUCache
-  private webSocketPingSetInterval?: NodeJS.Timeout
+  private wsPingSetInterval?: NodeJS.Timeout
   private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
   private flushMessageBufferSetInterval?: NodeJS.Timeout
 
-  constructor (index: number, templateFile: string) {
+  constructor (index: number, templateFile: string, options?: ChargingStationOptions) {
     super()
     this.started = false
     this.starting = false
     this.stopping = false
     this.wsConnection = null
-    this.autoReconnectRetryCount = 0
+    this.wsConnectionRetried = false
+    this.wsConnectionRetryCount = 0
     this.index = index
     this.templateFile = templateFile
     this.connectors = new Map<number, ConnectorStatus>()
@@ -204,6 +210,12 @@ export class ChargingStation extends EventEmitter {
     this.idTagsCache = IdTagsCache.getInstance()
     this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this)
 
+    this.on(ChargingStationEvents.added, () => {
+      parentPort?.postMessage(buildAddedMessage(this))
+    })
+    this.on(ChargingStationEvents.deleted, () => {
+      parentPort?.postMessage(buildDeletedMessage(this))
+    })
     this.on(ChargingStationEvents.started, () => {
       parentPort?.postMessage(buildStartedMessage(this))
     })
@@ -215,12 +227,16 @@ export class ChargingStation extends EventEmitter {
     })
     this.on(ChargingStationEvents.accepted, () => {
       this.startMessageSequence(
-        this.autoReconnectRetryCount > 0
+        this.wsConnectionRetried
           ? true
           : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
       ).catch(error => {
         logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
       })
+      this.wsConnectionRetried = false
+    })
+    this.on(ChargingStationEvents.rejected, () => {
+      this.wsConnectionRetried = false
     })
     this.on(ChargingStationEvents.disconnected, () => {
       try {
@@ -233,14 +249,25 @@ export class ChargingStation extends EventEmitter {
       }
     })
 
-    this.initialize()
+    this.initialize(options)
+
+    this.add()
+
+    if (options?.autoStart != null) {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      this.stationInfo!.autoStart = options.autoStart
+    }
+
+    if (this.stationInfo?.autoStart === true) {
+      this.start()
+    }
   }
 
   public get hasEvses (): boolean {
     return this.connectors.size === 0 && this.evses.size > 0
   }
 
-  private get wsConnectionUrl (): URL {
+  public get wsConnectionUrl (): URL {
     return new URL(
       `${
         this.stationInfo?.supervisionUrlOcppConfiguration === true &&
@@ -640,6 +667,28 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
+  private add (): void {
+    this.emit(ChargingStationEvents.added)
+  }
+
+  public async delete (deleteConfiguration = true): Promise<void> {
+    if (this.started) {
+      await this.stop()
+    }
+    AutomaticTransactionGenerator.deleteInstance(this)
+    PerformanceStatistics.deleteInstance(this.stationInfo?.hashId)
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
+    this.requests.clear()
+    this.connectors.clear()
+    this.evses.clear()
+    this.templateFileWatcher?.unref()
+    deleteConfiguration && rmSync(this.configurationFile, { force: true })
+    this.chargingStationWorkerBroadcastChannel.unref()
+    this.emit(ChargingStationEvents.deleted)
+    this.removeAllListeners()
+  }
+
   public start (): void {
     if (!this.started) {
       if (!this.starting) {
@@ -663,10 +712,10 @@ export class ChargingStation extends EventEmitter {
                   } file have changed, reload`
                 )
                 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
-                // Initialize
-                this.initialize()
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
+                // Initialize
+                this.initialize()
                 // Restart the ATG
                 const ATGStarted = this.automaticTransactionGenerator?.started
                 if (ATGStarted === true) {
@@ -705,7 +754,10 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
-  public async stop (reason?: StopTransactionReason, stopTransactions?: boolean): Promise<void> {
+  public async stop (
+    reason?: StopTransactionReason,
+    stopTransactions = this.stationInfo?.stopTransactionsOnStopped
+  ): Promise<void> {
     if (this.started) {
       if (!this.stopping) {
         this.stopping = true
@@ -714,12 +766,11 @@ export class ChargingStation extends EventEmitter {
         if (this.stationInfo?.enableStatistics === true) {
           this.performanceStatistics?.stop()
         }
-        this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
         this.templateFileWatcher?.close()
-        this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
         delete this.bootNotificationResponse
         this.started = false
         this.saveConfiguration()
+        this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
         this.emit(ChargingStationEvents.stopped)
         this.stopping = false
       } else {
@@ -774,14 +825,12 @@ export class ChargingStation extends EventEmitter {
 
     if (this.isWebSocketConnectionOpened()) {
       logger.warn(
-        `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
+        `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
       )
       return
     }
 
-    logger.info(
-      `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
-    )
+    logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
 
     this.wsConnection = new WebSocket(
       this.wsConnectionUrl,
@@ -1120,8 +1169,8 @@ export class ChargingStation extends EventEmitter {
     }
     const stationInfo = stationTemplateToStationInfo(stationTemplate)
     stationInfo.hashId = getHashId(this.index, stationTemplate)
+    stationInfo.templateName = parse(this.templateFile).name
     stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
-    stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16
     createSerialNumber(stationTemplate, stationInfo)
     stationInfo.voltageOut = this.getVoltageOut(stationInfo)
     if (isNotEmptyArray(stationTemplate.power)) {
@@ -1138,9 +1187,8 @@ export class ChargingStation extends EventEmitter {
           : stationTemplate.power
     }
     stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
-    stationInfo.firmwareVersionPattern =
-      stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN
     if (
+      isNotEmptyString(stationInfo.firmwareVersionPattern) &&
       isNotEmptyString(stationInfo.firmwareVersion) &&
       !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
     ) {
@@ -1159,29 +1207,34 @@ export class ChargingStation extends EventEmitter {
       },
       stationTemplate.firmwareUpgrade ?? {}
     )
-    stationInfo.resetTime =
-      stationTemplate.resetTime != null
-        ? secondsToMilliseconds(stationTemplate.resetTime)
-        : Constants.CHARGING_STATION_DEFAULT_RESET_TIME
+    if (stationTemplate.resetTime != null) {
+      stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
+    }
     return stationInfo
   }
 
   private getStationInfoFromFile (
-    stationInfoPersistentConfiguration = true
+    stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO
+      .stationInfoPersistentConfiguration
   ): ChargingStationInfo | undefined {
     let stationInfo: ChargingStationInfo | undefined
-    if (stationInfoPersistentConfiguration) {
+    if (stationInfoPersistentConfiguration === true) {
       stationInfo = this.getConfigurationFromFile()?.stationInfo
       if (stationInfo != null) {
         delete stationInfo.infoHash
+        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+        if (stationInfo.templateName == null) {
+          stationInfo.templateName = parse(this.templateFile).name
+        }
       }
     }
     return stationInfo
   }
 
-  private getStationInfo (): ChargingStationInfo {
-    const defaultStationInfo = Constants.DEFAULT_STATION_INFO
+  private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo {
     const stationInfoFromTemplate = this.getStationInfoFromTemplate()
+    options?.persistentConfiguration != null &&
+      (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration)
     const stationInfoFromFile = this.getStationInfoFromFile(
       stationInfoFromTemplate.stationInfoPersistentConfiguration
     )
@@ -1192,7 +1245,10 @@ export class ChargingStation extends EventEmitter {
       stationInfoFromFile != null &&
       stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
     ) {
-      return { ...defaultStationInfo, ...stationInfoFromFile }
+      return setChargingStationOptions(
+        { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromFile },
+        options
+      )
     }
     stationInfoFromFile != null &&
       propagateSerialNumber(
@@ -1200,7 +1256,10 @@ export class ChargingStation extends EventEmitter {
         stationInfoFromFile,
         stationInfoFromTemplate
       )
-    return { ...defaultStationInfo, ...stationInfoFromTemplate }
+    return setChargingStationOptions(
+      { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromTemplate },
+      options
+    )
   }
 
   private saveStationInfo (): void {
@@ -1215,7 +1274,7 @@ export class ChargingStation extends EventEmitter {
     throw new BaseError(errorMsg)
   }
 
-  private initialize (): void {
+  private initialize (options?: ChargingStationOptions): void {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const stationTemplate = this.getTemplateFromFile()!
     checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
@@ -1233,11 +1292,11 @@ export class ChargingStation extends EventEmitter {
     } else {
       this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
     }
-    this.stationInfo = this.getStationInfo()
+    this.stationInfo = this.getStationInfo(options)
     if (
       this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
-      isNotEmptyString(this.stationInfo.firmwareVersion) &&
-      isNotEmptyString(this.stationInfo.firmwareVersionPattern)
+      isNotEmptyString(this.stationInfo.firmwareVersionPattern) &&
+      isNotEmptyString(this.stationInfo.firmwareVersion)
     ) {
       const patternGroup =
         this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
@@ -1273,7 +1332,7 @@ export class ChargingStation extends EventEmitter {
     this.bootNotificationRequest = bootNotificationRequest
     this.powerDivider = this.getPowerDivider()
     // OCPP configuration
-    this.ocppConfiguration = this.getOcppConfiguration()
+    this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration)
     this.initializeOcppConfiguration()
     this.initializeOcppServices()
     if (this.stationInfo.autoRegister === true) {
@@ -1668,10 +1727,7 @@ export class ChargingStation extends EventEmitter {
           configurationData,
           buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
         )
-        if (
-          this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === false ||
-          this.getAutomaticTransactionGeneratorConfiguration() == null
-        ) {
+        if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) {
           delete configurationData.automaticTransactionGenerator
         }
         if (this.connectors.size > 0) {
@@ -1746,17 +1802,21 @@ export class ChargingStation extends EventEmitter {
     return this.getTemplateFromFile()?.Configuration
   }
 
-  private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration | undefined {
+  private getOcppConfigurationFromFile (
+    ocppPersistentConfiguration?: boolean
+  ): ChargingStationOcppConfiguration | undefined {
     const configurationKey = this.getConfigurationFromFile()?.configurationKey
-    if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
+    if (ocppPersistentConfiguration === true && Array.isArray(configurationKey)) {
       return { configurationKey }
     }
     return undefined
   }
 
-  private getOcppConfiguration (): ChargingStationOcppConfiguration | undefined {
+  private getOcppConfiguration (
+    ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration
+  ): ChargingStationOcppConfiguration | undefined {
     let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
-      this.getOcppConfigurationFromFile()
+      this.getOcppConfigurationFromFile(ocppPersistentConfiguration)
     if (ocppConfiguration == null) {
       ocppConfiguration = this.getOcppConfigurationFromTemplate()
     }
@@ -1766,7 +1826,7 @@ export class ChargingStation extends EventEmitter {
   private async onOpen (): Promise<void> {
     if (this.isWebSocketConnectionOpened()) {
       logger.info(
-        `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
+        `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded`
       )
       let registrationRetryCount = 0
       if (!this.isRegistered()) {
@@ -1816,11 +1876,11 @@ export class ChargingStation extends EventEmitter {
           })`
         )
       }
-      this.autoReconnectRetryCount = 0
+      this.wsConnectionRetryCount = 0
       this.emit(ChargingStationEvents.updated)
     } else {
       logger.warn(
-        `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
+        `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
       )
     }
   }
@@ -1836,7 +1896,7 @@ export class ChargingStation extends EventEmitter {
             code
           )}' and reason '${reason.toString()}'`
         )
-        this.autoReconnectRetryCount = 0
+        this.wsConnectionRetryCount = 0
         break
       // Abnormal close
       default:
@@ -1854,7 +1914,10 @@ export class ChargingStation extends EventEmitter {
     this.emit(ChargingStationEvents.updated)
   }
 
-  private getCachedRequest (messageType: MessageType, messageId: string): CachedRequest | undefined {
+  private getCachedRequest (
+    messageType: MessageType | undefined,
+    messageId: string
+  ): CachedRequest | undefined {
     const cachedRequest = this.requests.get(messageId)
     if (Array.isArray(cachedRequest)) {
       return cachedRequest
@@ -2006,7 +2069,7 @@ export class ChargingStation extends EventEmitter {
       if (!(error instanceof OCPPError)) {
         logger.warn(
           `${this.logPrefix()} Error thrown at incoming OCPP command '${
-            commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
+            commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
             // eslint-disable-next-line @typescript-eslint/no-base-to-string
           }' message '${data.toString()}' handling is not an OCPPError:`,
           error
@@ -2014,11 +2077,11 @@ export class ChargingStation extends EventEmitter {
       }
       logger.error(
         `${this.logPrefix()} Incoming OCPP command '${
-          commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
+          commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
           // eslint-disable-next-line @typescript-eslint/no-base-to-string
         }' message '${data.toString()}'${
-          messageType !== MessageType.CALL_MESSAGE
-            ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
+          this.requests.has(messageId)
+            ? ` matching cached request '${JSON.stringify(this.getCachedRequest(messageType, messageId))}'`
             : ''
         } processing error:`,
         error
@@ -2062,7 +2125,8 @@ export class ChargingStation extends EventEmitter {
   }
 
   private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
-    return stationTemplate?.useConnectorId0 ?? true
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
   }
 
   private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
@@ -2121,8 +2185,12 @@ export class ChargingStation extends EventEmitter {
   }
 
   private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    return (stationInfo ?? this.stationInfo!).currentOutType ?? CurrentType.AC
+    return (
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      (stationInfo ?? this.stationInfo!).currentOutType ??
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      Constants.DEFAULT_STATION_INFO.currentOutType!
+    )
   }
 
   private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
@@ -2211,7 +2279,7 @@ export class ChargingStation extends EventEmitter {
 
   private async stopMessageSequence (
     reason?: StopTransactionReason,
-    stopTransactions = this.stationInfo?.stopTransactionsOnStopped
+    stopTransactions?: boolean
   ): Promise<void> {
     this.internalStopMessageSequence()
     // Stop ongoing transactions
@@ -2247,8 +2315,8 @@ export class ChargingStation extends EventEmitter {
           getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
         )
         : 0
-    if (webSocketPingInterval > 0 && this.webSocketPingSetInterval == null) {
-      this.webSocketPingSetInterval = setInterval(() => {
+    if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
+      this.wsPingSetInterval = setInterval(() => {
         if (this.isWebSocketConnectionOpened()) {
           this.wsConnection?.ping()
         }
@@ -2258,7 +2326,7 @@ export class ChargingStation extends EventEmitter {
           webSocketPingInterval
         )}`
       )
-    } else if (this.webSocketPingSetInterval != null) {
+    } else if (this.wsPingSetInterval != null) {
       logger.info(
         `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
           webSocketPingInterval
@@ -2272,9 +2340,9 @@ export class ChargingStation extends EventEmitter {
   }
 
   private stopWebSocketPing (): void {
-    if (this.webSocketPingSetInterval != null) {
-      clearInterval(this.webSocketPingSetInterval)
-      delete this.webSocketPingSetInterval
+    if (this.wsPingSetInterval != null) {
+      clearInterval(this.wsPingSetInterval)
+      delete this.wsPingSetInterval
     }
   }
 
@@ -2333,13 +2401,14 @@ export class ChargingStation extends EventEmitter {
   private async reconnect (): Promise<void> {
     if (
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      this.autoReconnectRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
+      this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
       this.stationInfo?.autoReconnectMaxRetries === -1
     ) {
-      ++this.autoReconnectRetryCount
+      this.wsConnectionRetried = true
+      ++this.wsConnectionRetryCount
       const reconnectDelay =
         this.stationInfo?.reconnectExponentialDelay === true
-          ? exponentialDelay(this.autoReconnectRetryCount)
+          ? exponentialDelay(this.wsConnectionRetryCount)
           : secondsToMilliseconds(this.getConnectionTimeout())
       const reconnectDelayWithdraw = 1000
       const reconnectTimeout =
@@ -2352,7 +2421,7 @@ export class ChargingStation extends EventEmitter {
       )
       await sleep(reconnectDelay)
       logger.error(
-        `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
+        `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
       )
       this.openWSConnection(
         {
@@ -2363,7 +2432,7 @@ export class ChargingStation extends EventEmitter {
     } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
       logger.error(
         `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
-          this.autoReconnectRetryCount
+          this.wsConnectionRetryCount
         }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
       )
     }