fix: add charging station at instantiation
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 92fbb80c6e21bde391c74ffccdd52100a38bd26a..0f4bce43184793c9d3f7c221cb152f26dac5d9eb 100644 (file)
@@ -1,9 +1,9 @@
-// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
+// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
 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 { dirname, join, parse } from 'node:path'
 import { URL } from 'node:url'
 import { parentPort } from 'node:worker_threads'
 
@@ -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,
@@ -126,13 +123,14 @@ import {
   Configuration,
   Constants,
   DCElectricUtils,
+  buildAddedMessage,
   buildChargingStationAutomaticTransactionGeneratorConfiguration,
   buildConnectorsStatus,
   buildEvsesStatus,
   buildStartedMessage,
   buildStoppedMessage,
   buildUpdatedMessage,
-  cloneObject,
+  clone,
   convertToBoolean,
   convertToDate,
   convertToInt,
@@ -182,11 +180,12 @@ export class ChargingStation extends EventEmitter {
   private ocppIncomingRequestService!: OCPPIncomingRequestService
   private readonly messageBuffer: Set<string>
   private configuredSupervisionUrl!: URL
-  private autoReconnectRetryCount: number
+  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
 
@@ -196,7 +195,8 @@ export class ChargingStation extends EventEmitter {
     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>()
@@ -207,6 +207,9 @@ 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.started, () => {
       parentPort?.postMessage(buildStartedMessage(this))
     })
@@ -216,8 +219,35 @@ export class ChargingStation extends EventEmitter {
     this.on(ChargingStationEvents.updated, () => {
       parentPort?.postMessage(buildUpdatedMessage(this))
     })
+    this.on(ChargingStationEvents.accepted, () => {
+      this.startMessageSequence(
+        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 {
+        this.internalStopMessageSequence()
+      } catch (error) {
+        logger.error(
+          `${this.logPrefix()} Error while stopping the internal message sequence:`,
+          error
+        )
+      }
+    })
 
     this.initialize()
+
+    this.add()
+
+    this.stationInfo?.autoStart === true && this.start()
   }
 
   public get hasEvses (): boolean {
@@ -229,10 +259,8 @@ export class ChargingStation extends EventEmitter {
       `${
         this.stationInfo?.supervisionUrlOcppConfiguration === true &&
         isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)?.value)
-          ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-            getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)!.value
+        isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
+          ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
           : this.configuredSupervisionUrl.href
       }/${this.stationInfo?.chargingStationId}`
     )
@@ -504,8 +532,7 @@ export class ChargingStation extends EventEmitter {
       this.stationInfo?.supervisionUrlOcppConfiguration === true &&
       isNotEmptyString(this.stationInfo.supervisionUrlOcppKey)
     ) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey!, url)
+      setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
     } else {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       this.stationInfo!.supervisionUrls = url
@@ -627,6 +654,10 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
+  private add (): void {
+    this.emit(ChargingStationEvents.added)
+  }
+
   public start (): void {
     if (!this.started) {
       if (!this.starting) {
@@ -655,10 +686,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()
@@ -686,7 +723,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
@@ -771,26 +811,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 {
@@ -831,15 +868,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)) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      for (const connectorId of connectorIds!) {
-        this.automaticTransactionGenerator?.startConnector(connectorId)
+      for (const connectorId of connectorIds) {
+        this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration)
       }
     } else {
-      this.automaticTransactionGenerator?.start()
+      this.automaticTransactionGenerator?.start(stopAbsoluteDuration)
     }
     this.saveAutomaticTransactionGeneratorConfiguration()
     this.emit(ChargingStationEvents.updated)
@@ -847,8 +886,7 @@ export class ChargingStation extends EventEmitter {
 
   public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
     if (isNotEmptyArray(connectorIds)) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      for (const connectorId of connectorIds!) {
+      for (const connectorId of connectorIds) {
         this.automaticTransactionGenerator?.stopConnector(connectorId)
       }
     } else {
@@ -1103,22 +1141,23 @@ export class ChargingStation extends EventEmitter {
     }
     const stationInfo = stationTemplateToStationInfo(stationTemplate)
     stationInfo.hashId = getHashId(this.index, stationTemplate)
+    stationInfo.autoStart = stationTemplate.autoStart ?? true
+    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)) {
-      stationTemplate.power = stationTemplate.power as number[]
       const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
       stationInfo.maximumPower =
         stationTemplate.powerUnit === PowerUnits.KILO_WATT
           ? stationTemplate.power[powerArrayRandomIndex] * 1000
           : stationTemplate.power[powerArrayRandomIndex]
     } else {
-      stationTemplate.power = stationTemplate.power as number
       stationInfo.maximumPower =
         stationTemplate.powerUnit === PowerUnits.KILO_WATT
-          ? stationTemplate.power * 1000
+          ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          stationTemplate.power! * 1000
           : stationTemplate.power
     }
     stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
@@ -1126,8 +1165,7 @@ export class ChargingStation extends EventEmitter {
       stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN
     if (
       isNotEmptyString(stationInfo.firmwareVersion) &&
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion!)
+      !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
     ) {
       logger.warn(
         `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
@@ -1147,7 +1185,7 @@ export class ChargingStation extends EventEmitter {
     stationInfo.resetTime =
       stationTemplate.resetTime != null
         ? secondsToMilliseconds(stationTemplate.resetTime)
-        : Constants.CHARGING_STATION_DEFAULT_RESET_TIME
+        : Constants.DEFAULT_CHARGING_STATION_RESET_TIME
     return stationInfo
   }
 
@@ -1159,6 +1197,13 @@ export class ChargingStation extends EventEmitter {
       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
+        }
+        if (stationInfo.autoStart == null) {
+          stationInfo.autoStart = true
+        }
       }
     }
     return stationInfo
@@ -1226,13 +1271,10 @@ export class ChargingStation extends EventEmitter {
     ) {
       const patternGroup =
         this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
-        this.stationInfo.firmwareVersion?.split('.').length
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      const match = new RegExp(this.stationInfo.firmwareVersionPattern!)
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        .exec(this.stationInfo.firmwareVersion!)
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        ?.slice(1, patternGroup! + 1)
+        this.stationInfo.firmwareVersion.split('.').length
+      const match = new RegExp(this.stationInfo.firmwareVersionPattern)
+        .exec(this.stationInfo.firmwareVersion)
+        ?.slice(1, patternGroup + 1)
       if (match != null) {
         const patchLevelIndex = match.length - 1
         match[patchLevelIndex] = (
@@ -1264,11 +1306,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(),
@@ -1312,37 +1349,31 @@ export class ChargingStation extends EventEmitter {
     if (
       this.stationInfo?.supervisionUrlOcppConfiguration === true &&
       isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!) == null
+      getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null
     ) {
       addConfigurationKey(
         this,
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        this.stationInfo.supervisionUrlOcppKey!,
+        this.stationInfo.supervisionUrlOcppKey,
         this.configuredSupervisionUrl.href,
         { reboot: true }
       )
     } else if (
       this.stationInfo?.supervisionUrlOcppConfiguration === false &&
       isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!) != null
+      getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
     ) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!, { save: false })
+      deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false })
     }
     if (
       isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!) == null
+      getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null
     ) {
       addConfigurationKey(
         this,
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        this.stationInfo!.amperageLimitationOcppKey!,
+        this.stationInfo.amperageLimitationOcppKey,
         // prettier-ignore
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        (this.stationInfo!.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
+        (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
       )
     }
     if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
@@ -1413,11 +1444,11 @@ 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, cloneObject<ConnectorStatus>(connectorStatus))
+        this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
       }
     } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
       for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
-        const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration)
+        const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
         delete evseStatus.connectorsStatus
         this.evses.set(evseId, {
           ...(evseStatus as EvseStatus),
@@ -1504,7 +1535,7 @@ export class ChargingStation extends EventEmitter {
               this.logPrefix(),
               this.templateFile
             )
-            this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus))
+            this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
           }
           initializeConnectorsMapStatus(this.connectors, this.logPrefix())
           this.saveConnectorsStatus()
@@ -1648,7 +1679,7 @@ export class ChargingStation extends EventEmitter {
         const configurationFromFile = this.getConfigurationFromFile()
         let configurationData: ChargingStationConfiguration =
           configurationFromFile != null
-            ? cloneObject<ChargingStationConfiguration>(configurationFromFile)
+            ? clone<ChargingStationConfiguration>(configurationFromFile)
             : {}
         if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
           configurationData.stationInfo = this.stationInfo
@@ -1777,10 +1808,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(
-            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(
@@ -1803,13 +1837,16 @@ 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
           })`
         )
       }
-      this.autoReconnectRetryCount = 0
+      this.wsConnectionRetryCount = 0
       this.emit(ChargingStationEvents.updated)
     } else {
       logger.warn(
@@ -1818,7 +1855,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:
@@ -1828,7 +1866,7 @@ export class ChargingStation extends EventEmitter {
             code
           )}' and reason '${reason.toString()}'`
         )
-        this.autoReconnectRetryCount = 0
+        this.wsConnectionRetryCount = 0
         break
       // Abnormal close
       default:
@@ -1837,13 +1875,19 @@ 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)
   }
 
-  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
@@ -1965,11 +2009,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
@@ -2003,8 +2050,8 @@ export class ChargingStation extends EventEmitter {
           commandName ?? requestCommandName ?? Constants.UNKNOWN_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
@@ -2122,20 +2169,16 @@ export class ChargingStation extends EventEmitter {
   private getAmperageLimitation (): number | undefined {
     if (
       isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      getConfigurationKey(this, this.stationInfo!.amperageLimitationOcppKey!) != null
+      getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
     ) {
       return (
-        convertToInt(
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          getConfigurationKey(this, this.stationInfo!.amperageLimitationOcppKey!)!.value
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        ) / getAmperageLimitationUnitDivider(this.stationInfo!)
+        convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
+        getAmperageLimitationUnitDivider(this.stationInfo)
       )
     }
   }
 
-  private async startMessageSequence (): Promise<void> {
+  private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
     if (this.stationInfo?.autoRegister === true) {
       await this.ocppRequestService.requestHandler<
       BootNotificationRequest,
@@ -2183,15 +2226,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
@@ -2200,24 +2240,24 @@ export class ChargingStation extends EventEmitter {
     if (this.automaticTransactionGenerator?.started === true) {
       this.stopAutomaticTransactionGenerator()
     }
+  }
+
+  private async stopMessageSequence (
+    reason?: StopTransactionReason,
+    stopTransactions?: boolean
+  ): 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
           }
@@ -2226,14 +2266,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
         }
       }
@@ -2247,8 +2280,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 +2291,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 +2305,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
     }
   }
 
@@ -2285,9 +2318,7 @@ export class ChargingStation extends EventEmitter {
       let configuredSupervisionUrlIndex: number
       switch (Configuration.getSupervisionUrlDistribution()) {
         case SupervisionUrlDistribution.RANDOM:
-          configuredSupervisionUrlIndex = Math.floor(
-            secureRandom() * (supervisionUrls as string[]).length
-          )
+          configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
           break
         case SupervisionUrlDistribution.ROUND_ROBIN:
         case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
@@ -2302,12 +2333,13 @@ export class ChargingStation extends EventEmitter {
                 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
               }`
             )
-          configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length
+          configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
           break
       }
-      configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex]
+      configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
     } else {
-      configuredSupervisionUrl = supervisionUrls as string
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      configuredSupervisionUrl = supervisionUrls!
     }
     if (isNotEmptyString(configuredSupervisionUrl)) {
       return new URL(configuredSupervisionUrl)
@@ -2332,23 +2364,16 @@ 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! ||
+      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 =
@@ -2361,7 +2386,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(
         {
@@ -2372,7 +2397,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})`
       )
     }