build(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 2d934e066ff07a58470dc6162c342491df1adb93..e217991ac8aad3c7c4256010687929df0f0162a3 100644 (file)
@@ -120,8 +120,9 @@ import {
   createSerialNumber,
   getAmperageLimitationUnitDivider,
   getBootConnectorStatus,
-  getChargingStationConnectorChargingProfilesPowerLimit,
+  getChargingStationChargingProfilesLimit,
   getChargingStationId,
+  getConnectorChargingProfilesLimit,
   getDefaultVoltageOut,
   getHashId,
   getIdTagsFile,
@@ -131,6 +132,7 @@ import {
   hasFeatureProfile,
   hasReservationExpired,
   initializeConnectorsMapStatus,
+  prepareConnectorStatus,
   propagateSerialNumber,
   setChargingStationOptions,
   stationTemplateToStationInfo,
@@ -181,7 +183,6 @@ export class ChargingStation extends EventEmitter {
   private ocppIncomingRequestService!: OCPPIncomingRequestService
   private readonly messageBuffer: Set<string>
   private configuredSupervisionUrl!: URL
-  private wsConnectionRetried: boolean
   private wsConnectionRetryCount: number
   private templateFileWatcher?: FSWatcher
   private templateFileHash!: string
@@ -196,7 +197,6 @@ export class ChargingStation extends EventEmitter {
     this.starting = false
     this.stopping = false
     this.wsConnection = null
-    this.wsConnectionRetried = false
     this.wsConnectionRetryCount = 0
     this.index = index
     this.templateFile = templateFile
@@ -225,16 +225,21 @@ export class ChargingStation extends EventEmitter {
     })
     this.on(ChargingStationEvents.accepted, () => {
       this.startMessageSequence(
-        this.wsConnectionRetried
+        this.wsConnectionRetryCount > 0
           ? true
           : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
       ).catch((error: unknown) => {
         logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
       })
-      this.wsConnectionRetried = false
+      this.wsConnectionRetryCount = 0
     })
     this.on(ChargingStationEvents.rejected, () => {
-      this.wsConnectionRetried = false
+      this.wsConnectionRetryCount = 0
+    })
+    this.on(ChargingStationEvents.connected, () => {
+      if (this.wsPingSetInterval == null) {
+        this.startWebSocketPing()
+      }
     })
     this.on(ChargingStationEvents.disconnected, () => {
       try {
@@ -387,14 +392,14 @@ export class ChargingStation extends EventEmitter {
   }
 
   public getConnectorMaximumAvailablePower (connectorId: number): number {
-    let connectorAmperageLimitationPowerLimit: number | undefined
+    let connectorAmperageLimitationLimit: number | undefined
     const amperageLimitation = this.getAmperageLimitation()
     if (
       amperageLimitation != null &&
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       amperageLimitation < this.stationInfo!.maximumAmperage!
     ) {
-      connectorAmperageLimitationPowerLimit =
+      connectorAmperageLimitationLimit =
         (this.stationInfo?.currentOutType === CurrentType.AC
           ? ACElectricUtils.powerTotal(
             this.getNumberOfPhases(),
@@ -410,20 +415,25 @@ export class ChargingStation extends EventEmitter {
     }
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider!
-    const connectorChargingProfilesPowerLimit =
-      getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId)
+    const chargingStationChargingProfilesLimit =
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      getChargingStationChargingProfilesLimit(this)! / this.powerDivider!
+    const connectorChargingProfilesLimit = getConnectorChargingProfilesLimit(this, connectorId)
     return min(
       isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower,
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      isNaN(connectorAmperageLimitationPowerLimit!)
+      isNaN(connectorAmperageLimitationLimit!)
         ? Number.POSITIVE_INFINITY
         : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        connectorAmperageLimitationPowerLimit!,
+        connectorAmperageLimitationLimit!,
+      isNaN(chargingStationChargingProfilesLimit)
+        ? Number.POSITIVE_INFINITY
+        : chargingStationChargingProfilesLimit,
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      isNaN(connectorChargingProfilesPowerLimit!)
+      isNaN(connectorChargingProfilesLimit!)
         ? Number.POSITIVE_INFINITY
         : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        connectorChargingProfilesPowerLimit!
+        connectorChargingProfilesLimit!
     )
   }
 
@@ -554,7 +564,8 @@ export class ChargingStation extends EventEmitter {
   }
 
   public startHeartbeat (): void {
-    if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval == null) {
+    const heartbeatInterval = this.getHeartbeatInterval()
+    if (heartbeatInterval > 0 && this.heartbeatSetInterval == null) {
       this.heartbeatSetInterval = setInterval(() => {
         this.ocppRequestService
           .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
@@ -564,21 +575,21 @@ export class ChargingStation extends EventEmitter {
               error
             )
           })
-      }, this.getHeartbeatInterval())
+      }, heartbeatInterval)
       logger.info(
         `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
-          this.getHeartbeatInterval()
+          heartbeatInterval
         )}`
       )
     } else if (this.heartbeatSetInterval != null) {
       logger.info(
         `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
-          this.getHeartbeatInterval()
+          heartbeatInterval
         )}`
       )
     } else {
       logger.error(
-        `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
+        `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval}, not starting the heartbeat`
       )
     }
   }
@@ -666,6 +677,11 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
+  public restartMeterValues (connectorId: number, interval: number): void {
+    this.stopMeterValues(connectorId)
+    this.startMeterValues(connectorId, interval)
+  }
+
   private add (): void {
     this.emit(ChargingStationEvents.added)
   }
@@ -1468,7 +1484,10 @@ export class ChargingStation extends EventEmitter {
   private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
     if (configuration.connectorsStatus != null && configuration.evsesStatus == null) {
       for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
-        this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
+        this.connectors.set(
+          connectorId,
+          prepareConnectorStatus(clone<ConnectorStatus>(connectorStatus))
+        )
       }
     } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
       for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
@@ -1480,7 +1499,7 @@ export class ChargingStation extends EventEmitter {
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
             evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
               connectorId,
-              connectorStatus
+              prepareConnectorStatus(connectorStatus)
             ])
           )
         })
@@ -1822,6 +1841,7 @@ export class ChargingStation extends EventEmitter {
 
   private async onOpen (): Promise<void> {
     if (this.isWebSocketConnectionOpened()) {
+      this.emit(ChargingStationEvents.connected)
       this.emit(ChargingStationEvents.updated)
       logger.info(
         `${this.logPrefix()} Connection to OCPP server through ${
@@ -1832,20 +1852,16 @@ export class ChargingStation extends EventEmitter {
       if (!this.isRegistered()) {
         // Send BootNotification
         do {
-          // FIXME: duplicated assignment with the boot notification response handler
-          this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
+          await this.ocppRequestService.requestHandler<
           BootNotificationRequest,
           BootNotificationResponse
           >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
             skipBufferingOnError: true
           })
-          // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-          if (this.bootNotificationResponse?.currentTime != null) {
-            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-            this.bootNotificationResponse.currentTime = convertToDate(
-              this.bootNotificationResponse.currentTime
-            )!
-          }
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          this.bootNotificationResponse!.currentTime = convertToDate(
+            this.bootNotificationResponse?.currentTime
+          )!
           if (!this.isRegistered()) {
             this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
             await sleep(
@@ -1869,7 +1885,6 @@ export class ChargingStation extends EventEmitter {
           })`
         )
       }
-      this.wsConnectionRetryCount = 0
       this.emit(ChargingStationEvents.updated)
     } else {
       logger.warn(
@@ -1921,7 +1936,7 @@ export class ChargingStation extends EventEmitter {
     }
     throw new OCPPError(
       ErrorType.PROTOCOL_ERROR,
-      `Cached request for message id ${messageId} ${getMessageTypeString(
+      `Cached request for message id '${messageId}' ${getMessageTypeString(
         messageType
       )} is not an array`,
       undefined,
@@ -1931,6 +1946,14 @@ export class ChargingStation extends EventEmitter {
 
   private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
     const [messageType, messageId, commandName, commandPayload] = request
+    if (this.requests.has(messageId)) {
+      throw new OCPPError(
+        ErrorType.SECURITY_ERROR,
+        `Received message with duplicate message id '${messageId}'`,
+        commandName,
+        commandPayload
+      )
+    }
     if (this.stationInfo?.enableStatistics === true) {
       this.performanceStatistics?.addRequestStatistic(commandName, messageType)
     }
@@ -1955,7 +1978,7 @@ export class ChargingStation extends EventEmitter {
       // Error
       throw new OCPPError(
         ErrorType.INTERNAL_ERROR,
-        `Response for unknown message id ${messageId}`,
+        `Response for unknown message id '${messageId}'`,
         undefined,
         commandPayload
       )
@@ -1980,7 +2003,7 @@ export class ChargingStation extends EventEmitter {
       // Error
       throw new OCPPError(
         ErrorType.INTERNAL_ERROR,
-        `Error response for unknown message id ${messageId}`,
+        `Error response for unknown message id '${messageId}'`,
         undefined,
         { errorType, errorMessage, errorDetails }
       )
@@ -2065,10 +2088,10 @@ export class ChargingStation extends EventEmitter {
       }
       if (!(error instanceof OCPPError)) {
         logger.warn(
-          `${this.logPrefix()} Error thrown at incoming OCPP command '${
+          `${this.logPrefix()} Error thrown at incoming OCPP 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:`,
+          } message '${data.toString()}' handling is not an OCPPError:`,
           error
         )
       }
@@ -2222,12 +2245,12 @@ export class ChargingStation extends EventEmitter {
       })
     }
     // Start WebSocket ping
-    this.startWebSocketPing()
+    if (this.wsPingSetInterval == null) {
+      this.startWebSocketPing()
+    }
     // Start heartbeat
     if (this.heartbeatSetInterval == null) {
       this.startHeartbeat()
-    } else if (this.getHeartbeatInterval() !== this.bootNotificationResponse?.interval) {
-      this.restartHeartbeat()
     }
     // Initialize connectors status
     if (this.hasEvses) {
@@ -2318,13 +2341,14 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
+  private getWebSocketPingInterval (): number {
+    return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
+      ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
+      : 0
+  }
+
   private startWebSocketPing (): void {
-    const webSocketPingInterval =
-      getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
-        ? convertToInt(
-          getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value
-        )
-        : 0
+    const webSocketPingInterval = this.getWebSocketPingInterval()
     if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
       this.wsPingSetInterval = setInterval(() => {
         if (this.isWebSocketConnectionOpened()) {
@@ -2414,7 +2438,6 @@ export class ChargingStation extends EventEmitter {
       this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
       this.stationInfo?.autoReconnectMaxRetries === -1
     ) {
-      this.wsConnectionRetried = true
       ++this.wsConnectionRetryCount
       const reconnectDelay =
         this.stationInfo?.reconnectExponentialDelay === true
@@ -2441,9 +2464,7 @@ export class ChargingStation extends EventEmitter {
       )
     } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
       logger.error(
-        `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
-          this.wsConnectionRetryCount
-        }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})`
+        `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`
       )
     }
   }