fix: properly handle template relative file path within a directory
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index c9c8208a32bbe797d1be024d40ddabaa85099fee..82fba5adad98c6629a6579b8e7c8516df38bb7bb 100644 (file)
@@ -2,13 +2,13 @@
 
 import { createHash } from 'node:crypto'
 import { EventEmitter } from 'node:events'
-import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
+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'
 
 import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns'
-import merge from 'just-merge'
+import { mergeDeepRight } from 'rambda'
 import { type RawData, WebSocket } from 'ws'
 
 import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js'
@@ -42,6 +42,7 @@ import {
   hasReservationExpired,
   initializeConnectorsMapStatus,
   propagateSerialNumber,
+  setChargingStationOptions,
   stationTemplateToStationInfo,
   warnTemplateKeysDeprecation
 } from './Helpers.js'
@@ -87,7 +88,6 @@ import {
   FirmwareStatus,
   type FirmwareStatusNotificationRequest,
   type FirmwareStatusNotificationResponse,
-  type FirmwareUpgrade,
   type HeartbeatRequest,
   type HeartbeatResponse,
   type IncomingRequest,
@@ -127,6 +127,7 @@ import {
   buildAddedMessage,
   buildChargingStationAutomaticTransactionGeneratorConfiguration,
   buildConnectorsStatus,
+  buildDeletedMessage,
   buildEvsesStatus,
   buildStartedMessage,
   buildStoppedMessage,
@@ -160,13 +161,13 @@ export class ChargingStation extends EventEmitter {
   public started: boolean
   public starting: boolean
   public idTagsCache: IdTagsCache
-  public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined
-  public ocppConfiguration!: ChargingStationOcppConfiguration | undefined
+  public automaticTransactionGenerator?: AutomaticTransactionGenerator
+  public ocppConfiguration?: ChargingStationOcppConfiguration
   public wsConnection: WebSocket | null
   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
   public heartbeatSetInterval?: NodeJS.Timeout
   public ocppRequestService!: OCPPRequestService
   public bootNotificationRequest?: BootNotificationRequest
@@ -183,7 +184,7 @@ export class ChargingStation extends EventEmitter {
   private configuredSupervisionUrl!: URL
   private wsConnectionRetried: boolean
   private wsConnectionRetryCount: number
-  private templateFileWatcher!: FSWatcher | undefined
+  private templateFileWatcher?: FSWatcher
   private templateFileHash!: string
   private readonly sharedLRUCache: SharedLRUCache
   private wsPingSetInterval?: NodeJS.Timeout
@@ -211,6 +212,9 @@ export class ChargingStation extends EventEmitter {
     this.on(ChargingStationEvents.added, () => {
       parentPort?.postMessage(buildAddedMessage(this))
     })
+    this.on(ChargingStationEvents.deleted, () => {
+      parentPort?.postMessage(buildDeletedMessage(this))
+    })
     this.on(ChargingStationEvents.started, () => {
       parentPort?.postMessage(buildStartedMessage(this))
     })
@@ -248,11 +252,6 @@ export class ChargingStation extends EventEmitter {
 
     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()
     }
@@ -544,8 +543,8 @@ export class ChargingStation extends EventEmitter {
     } else {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       this.stationInfo!.supervisionUrls = url
-      this.saveStationInfo()
       this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
+      this.saveStationInfo()
     }
   }
 
@@ -666,6 +665,24 @@ export class ChargingStation extends EventEmitter {
     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) {
@@ -689,10 +706,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) {
@@ -743,12 +760,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 {
@@ -1016,15 +1032,14 @@ export class ChargingStation extends EventEmitter {
     connectorId?: number
   ): boolean {
     const reservation = this.getReservationBy('reservationId', reservationId)
-    const reservationExists = reservation !== undefined && !hasReservationExpired(reservation)
+    const reservationExists = reservation != null && !hasReservationExpired(reservation)
     if (arguments.length === 1) {
       return !reservationExists
     } else if (arguments.length > 1) {
-      const userReservation =
-        idTag !== undefined ? this.getReservationBy('idTag', idTag) : undefined
+      const userReservation = idTag != null ? this.getReservationBy('idTag', idTag) : undefined
       const userReservationExists =
-        userReservation !== undefined && !hasReservationExpired(userReservation)
-      const notConnectorZero = connectorId === undefined ? true : connectorId > 0
+        userReservation != null && !hasReservationExpired(userReservation)
+      const notConnectorZero = connectorId == null ? true : connectorId > 0
       const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
       return (
         !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
@@ -1147,10 +1162,9 @@ export class ChargingStation extends EventEmitter {
     }
     const stationInfo = stationTemplateToStationInfo(stationTemplate)
     stationInfo.hashId = getHashId(this.index, stationTemplate)
-    stationInfo.autoStart = stationTemplate.autoStart ?? true
+    stationInfo.templateIndex = this.index
     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)) {
@@ -1167,9 +1181,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)
     ) {
@@ -1179,7 +1192,7 @@ export class ChargingStation extends EventEmitter {
         } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
       )
     }
-    stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
+    stationInfo.firmwareUpgrade = mergeDeepRight(
       {
         versionUpgrade: {
           step: 1
@@ -1188,10 +1201,9 @@ export class ChargingStation extends EventEmitter {
       },
       stationTemplate.firmwareUpgrade ?? {}
     )
-    stationInfo.resetTime =
-      stationTemplate.resetTime != null
-        ? secondsToMilliseconds(stationTemplate.resetTime)
-        : Constants.DEFAULT_CHARGING_STATION_RESET_TIME
+    if (stationTemplate.resetTime != null) {
+      stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
+    }
     return stationInfo
   }
 
@@ -1204,23 +1216,24 @@ export class ChargingStation extends EventEmitter {
       stationInfo = this.getConfigurationFromFile()?.stationInfo
       if (stationInfo != null) {
         delete stationInfo.infoHash
+        delete (stationInfo as ChargingStationTemplate).numberOfConnectors
+        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+        if (stationInfo.templateIndex == null) {
+          stationInfo.templateIndex = this.index
+        }
         // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
         if (stationInfo.templateName == null) {
           stationInfo.templateName = parse(this.templateFile).name
         }
-        if (stationInfo.autoStart == null) {
-          stationInfo.autoStart = true
-        }
       }
     }
     return stationInfo
   }
 
-  private getStationInfo (stationInfoPersistentConfiguration?: boolean): ChargingStationInfo {
+  private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo {
     const stationInfoFromTemplate = this.getStationInfoFromTemplate()
-    stationInfoPersistentConfiguration != null &&
-      (stationInfoFromTemplate.stationInfoPersistentConfiguration =
-        stationInfoPersistentConfiguration)
+    options?.persistentConfiguration != null &&
+      (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration)
     const stationInfoFromFile = this.getStationInfoFromFile(
       stationInfoFromTemplate.stationInfoPersistentConfiguration
     )
@@ -1231,7 +1244,10 @@ export class ChargingStation extends EventEmitter {
       stationInfoFromFile != null &&
       stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
     ) {
-      return { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromFile }
+      return setChargingStationOptions(
+        { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromFile },
+        options
+      )
     }
     stationInfoFromFile != null &&
       propagateSerialNumber(
@@ -1239,7 +1255,10 @@ export class ChargingStation extends EventEmitter {
         stationInfoFromFile,
         stationInfoFromTemplate
       )
-    return { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromTemplate }
+    return setChargingStationOptions(
+      { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromTemplate },
+      options
+    )
   }
 
   private saveStationInfo (): void {
@@ -1272,27 +1291,11 @@ export class ChargingStation extends EventEmitter {
     } else {
       this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
     }
-    this.stationInfo = this.getStationInfo(options?.persistentConfiguration)
-    if (options?.persistentConfiguration != null) {
-      this.stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
-    }
-    if (options?.persistentConfiguration != null) {
-      this.stationInfo.automaticTransactionGeneratorPersistentConfiguration =
-        options.persistentConfiguration
-    }
-    if (options?.autoRegister != null) {
-      this.stationInfo.autoRegister = options.autoRegister
-    }
-    if (options?.enableStatistics != null) {
-      this.stationInfo.enableStatistics = options.enableStatistics
-    }
-    if (options?.ocppStrictCompliance != null) {
-      this.stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
-    }
+    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 ??
@@ -1719,7 +1722,7 @@ export class ChargingStation extends EventEmitter {
         } else {
           delete configurationData.configurationKey
         }
-        configurationData = merge<ChargingStationConfiguration>(
+        configurationData = mergeDeepRight(
           configurationData,
           buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
         )
@@ -1809,8 +1812,7 @@ export class ChargingStation extends EventEmitter {
   }
 
   private getOcppConfiguration (
-    ocppPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO
-      .ocppPersistentConfiguration
+    ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration
   ): ChargingStationOcppConfiguration | undefined {
     let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
       this.getOcppConfigurationFromFile(ocppPersistentConfiguration)
@@ -1822,6 +1824,7 @@ export class ChargingStation extends EventEmitter {
 
   private async onOpen (): Promise<void> {
     if (this.isWebSocketConnectionOpened()) {
+      this.emit(ChargingStationEvents.updated)
       logger.info(
         `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded`
       )
@@ -1884,6 +1887,7 @@ export class ChargingStation extends EventEmitter {
 
   private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
     this.emit(ChargingStationEvents.disconnected)
+    this.emit(ChargingStationEvents.updated)
     switch (code) {
       // Normal close
       case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
@@ -1903,12 +1907,13 @@ export class ChargingStation extends EventEmitter {
           )}' and reason '${reason.toString()}'`
         )
         this.started &&
-          this.reconnect().catch(error =>
-            logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
-          )
+          this.reconnect()
+            .then(() => {
+              this.emit(ChargingStationEvents.updated)
+            })
+            .catch(error => logger.error(`${this.logPrefix()} Error while reconnecting:`, error))
         break
     }
-    this.emit(ChargingStationEvents.updated)
   }
 
   private getCachedRequest (
@@ -2066,7 +2071,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
@@ -2074,7 +2079,7 @@ 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()}'${
           this.requests.has(messageId)
@@ -2122,7 +2127,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> {
@@ -2181,8 +2187,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 {