fix: ensure the ATG is properly restored after disconnection to CSMS
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 7534f66e72f38abd778d11397b42708a03ab0af3..ba2f30c89f47312fe2f49580b4cc9298d81a806d 100644 (file)
@@ -56,7 +56,6 @@ import {
   type OCPPIncomingRequestService,
   type OCPPRequestService,
   buildMeterValue,
-  buildStatusNotificationRequest,
   buildTransactionEndMeterValue,
   getMessageTypeString,
   sendAndSetConnectorStatus
@@ -107,8 +106,6 @@ import {
   type Response,
   StandardParametersKey,
   type Status,
-  type StatusNotificationRequest,
-  type StatusNotificationResponse,
   type StopTransactionReason,
   type StopTransactionRequest,
   type StopTransactionResponse,
@@ -216,6 +213,25 @@ export class ChargingStation extends EventEmitter {
     this.on(ChargingStationEvents.updated, () => {
       parentPort?.postMessage(buildUpdatedMessage(this))
     })
+    this.on(ChargingStationEvents.accepted, () => {
+      this.startMessageSequence(
+        this.autoReconnectRetryCount > 0
+          ? true
+          : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration
+      ).catch(error => {
+        logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
+      })
+    })
+    this.on(ChargingStationEvents.disconnected, () => {
+      try {
+        this.internalStopMessageSequence()
+      } catch (error) {
+        logger.error(
+          `${this.logPrefix()} Error while stopping the internal message sequence:`,
+          error
+        )
+      }
+    })
 
     this.initialize()
   }
@@ -652,10 +668,16 @@ export class ChargingStation extends EventEmitter {
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                 this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
                 // Restart the ATG
-                this.stopAutomaticTransactionGenerator()
+                const ATGStarted = this.automaticTransactionGenerator?.started
+                if (ATGStarted === true) {
+                  this.stopAutomaticTransactionGenerator()
+                }
                 delete this.automaticTransactionGeneratorConfiguration
-                if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
-                  this.startAutomaticTransactionGenerator()
+                if (
+                  this.getAutomaticTransactionGeneratorConfiguration()?.enable === true &&
+                  ATGStarted === true
+                ) {
+                  this.startAutomaticTransactionGenerator(undefined, true)
                 }
                 if (this.stationInfo?.enableStatistics === true) {
                   this.performanceStatistics?.restart()
@@ -768,26 +790,23 @@ export class ChargingStation extends EventEmitter {
     )
 
     // Handle WebSocket message
-    this.wsConnection.on(
-      'message',
-      this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
-    )
+    this.wsConnection.on('message', data => {
+      this.onMessage(data).catch(Constants.EMPTY_FUNCTION)
+    })
     // Handle WebSocket error
-    this.wsConnection.on(
-      'error',
-      this.onError.bind(this) as (this: WebSocket, error: Error) => void
-    )
+    this.wsConnection.on('error', this.onError.bind(this))
     // Handle WebSocket close
-    this.wsConnection.on(
-      'close',
-      this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
-    )
+    this.wsConnection.on('close', this.onClose.bind(this))
     // Handle WebSocket open
-    this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void)
+    this.wsConnection.on('open', () => {
+      this.onOpen().catch(error =>
+        logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
+      )
+    })
     // Handle WebSocket ping
-    this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void)
+    this.wsConnection.on('ping', this.onPing.bind(this))
     // Handle WebSocket pong
-    this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void)
+    this.wsConnection.on('pong', this.onPong.bind(this))
   }
 
   public closeWSConnection (): void {
@@ -828,14 +847,17 @@ export class ChargingStation extends EventEmitter {
     return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
   }
 
-  public startAutomaticTransactionGenerator (connectorIds?: number[]): void {
+  public startAutomaticTransactionGenerator (
+    connectorIds?: number[],
+    stopAbsoluteDuration?: boolean
+  ): void {
     this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
     if (isNotEmptyArray(connectorIds)) {
       for (const connectorId of connectorIds) {
-        this.automaticTransactionGenerator?.startConnector(connectorId)
+        this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration)
       }
     } else {
-      this.automaticTransactionGenerator?.start()
+      this.automaticTransactionGenerator?.start(stopAbsoluteDuration)
     }
     this.saveAutomaticTransactionGeneratorConfiguration()
     this.emit(ChargingStationEvents.updated)
@@ -1254,11 +1276,6 @@ export class ChargingStation extends EventEmitter {
     this.ocppConfiguration = this.getOcppConfiguration()
     this.initializeOcppConfiguration()
     this.initializeOcppServices()
-    this.once(ChargingStationEvents.accepted, () => {
-      this.startMessageSequence().catch(error => {
-        logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error)
-      })
-    })
     if (this.stationInfo.autoRegister === true) {
       this.bootNotificationResponse = {
         currentTime: new Date(),
@@ -1761,11 +1778,13 @@ export class ChargingStation extends EventEmitter {
           >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
             skipBufferingOnError: true
           })
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          this.bootNotificationResponse.currentTime = convertToDate(
-            // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-            this.bootNotificationResponse?.currentTime
-          )!
+          // 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
+            )!
+          }
           if (!this.isRegistered()) {
             this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
             await sleep(
@@ -1788,6 +1807,9 @@ export class ChargingStation extends EventEmitter {
           this.emit(ChargingStationEvents.accepted)
         }
       } else {
+        if (this.inRejectedState()) {
+          this.emit(ChargingStationEvents.rejected)
+        }
         logger.error(
           `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
             this.stationInfo?.registrationMaxRetries
@@ -1803,7 +1825,8 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
-  private async onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): Promise<void> {
+  private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
+    this.emit(ChargingStationEvents.disconnected)
     switch (code) {
       // Normal close
       case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
@@ -1822,7 +1845,10 @@ export class ChargingStation extends EventEmitter {
             code
           )}' and reason '${reason.toString()}'`
         )
-        this.started && (await this.reconnect())
+        this.started &&
+          this.reconnect().catch(error =>
+            logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
+          )
         break
     }
     this.emit(ChargingStationEvents.updated)
@@ -1950,11 +1976,14 @@ export class ChargingStation extends EventEmitter {
         )
       }
     } catch (error) {
+      if (!Array.isArray(request)) {
+        logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
+        return
+      }
       let commandName: IncomingRequestCommand | undefined
       let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
       let errorCallback: ErrorCallback
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      const [, messageId] = request!
+      const [, messageId] = request
       switch (messageType) {
         case MessageType.CALL_MESSAGE:
           [, , commandName] = request as IncomingRequest
@@ -2116,7 +2145,7 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
-  private async startMessageSequence (): Promise<void> {
+  private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
     if (this.stationInfo?.autoRegister === true) {
       await this.ocppRequestService.requestHandler<
       BootNotificationRequest,
@@ -2164,15 +2193,12 @@ export class ChargingStation extends EventEmitter {
 
     // Start the ATG
     if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
-      this.startAutomaticTransactionGenerator()
+      this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
     }
     this.flushMessageBuffer()
   }
 
-  private async stopMessageSequence (
-    reason?: StopTransactionReason,
-    stopTransactions = this.stationInfo?.stopTransactionsOnStopped
-  ): Promise<void> {
+  private internalStopMessageSequence (): void {
     // Stop WebSocket ping
     this.stopWebSocketPing()
     // Stop heartbeat
@@ -2181,24 +2207,24 @@ export class ChargingStation extends EventEmitter {
     if (this.automaticTransactionGenerator?.started === true) {
       this.stopAutomaticTransactionGenerator()
     }
+  }
+
+  private async stopMessageSequence (
+    reason?: StopTransactionReason,
+    stopTransactions = this.stationInfo?.stopTransactionsOnStopped
+  ): Promise<void> {
+    this.internalStopMessageSequence()
     // Stop ongoing transactions
     stopTransactions === true && (await this.stopRunningTransactions(reason))
     if (this.hasEvses) {
       for (const [evseId, evseStatus] of this.evses) {
         if (evseId > 0) {
           for (const [connectorId, connectorStatus] of evseStatus.connectors) {
-            await this.ocppRequestService.requestHandler<
-            StatusNotificationRequest,
-            StatusNotificationResponse
-            >(
+            await sendAndSetConnectorStatus(
               this,
-              RequestCommand.STATUS_NOTIFICATION,
-              buildStatusNotificationRequest(
-                this,
-                connectorId,
-                ConnectorStatusEnum.Unavailable,
-                evseId
-              )
+              connectorId,
+              ConnectorStatusEnum.Unavailable,
+              evseId
             )
             delete connectorStatus.status
           }
@@ -2207,14 +2233,7 @@ export class ChargingStation extends EventEmitter {
     } else {
       for (const connectorId of this.connectors.keys()) {
         if (connectorId > 0) {
-          await this.ocppRequestService.requestHandler<
-          StatusNotificationRequest,
-          StatusNotificationResponse
-          >(
-            this,
-            RequestCommand.STATUS_NOTIFICATION,
-            buildStatusNotificationRequest(this, connectorId, ConnectorStatusEnum.Unavailable)
-          )
+          await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
           delete this.getConnectorStatus(connectorId)?.status
         }
       }
@@ -2312,14 +2331,6 @@ export class ChargingStation extends EventEmitter {
   }
 
   private async reconnect (): Promise<void> {
-    // Stop WebSocket ping
-    this.stopWebSocketPing()
-    // Stop heartbeat
-    this.stopHeartbeat()
-    // Stop the ATG if needed
-    if (this.getAutomaticTransactionGeneratorConfiguration()?.stopOnConnectionFailure === true) {
-      this.stopAutomaticTransactionGenerator()
-    }
     if (
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       this.autoReconnectRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||