build(deps-dev): apply updates
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index a3b0328d4470a00c4eb8acf5f84d9f917a7e0410..e217991ac8aad3c7c4256010687929df0f0162a3 100644 (file)
@@ -1,67 +1,16 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
-import { createHash } from 'node:crypto'
+import { createHash, randomInt } from 'node:crypto'
 import { EventEmitter } from 'node:events'
-import { type FSWatcher, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
-import { dirname, join, parse } from 'node:path'
+import { existsSync, type FSWatcher, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
 import { URL } from 'node:url'
 import { parentPort } from 'node:worker_threads'
 
 import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns'
-import { merge } from 'lodash-es'
+import { mergeDeepRight, once } from 'rambda'
 import { type RawData, WebSocket } from 'ws'
 
-import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js'
-import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel.js'
-import {
-  addConfigurationKey,
-  deleteConfigurationKey,
-  getConfigurationKey,
-  setConfigurationKeyValue
-} from './ConfigurationKeyUtils.js'
-import {
-  buildConnectorsMap,
-  checkChargingStation,
-  checkConfiguration,
-  checkConnectorsConfiguration,
-  checkStationInfoConnectorStatus,
-  checkTemplate,
-  createBootNotificationRequest,
-  createSerialNumber,
-  getAmperageLimitationUnitDivider,
-  getBootConnectorStatus,
-  getChargingStationConnectorChargingProfilesPowerLimit,
-  getChargingStationId,
-  getDefaultVoltageOut,
-  getHashId,
-  getIdTagsFile,
-  getMaxNumberOfEvses,
-  getNumberOfReservableConnectors,
-  getPhaseRotationValue,
-  hasFeatureProfile,
-  hasReservationExpired,
-  initializeConnectorsMapStatus,
-  propagateSerialNumber,
-  setChargingStationOptions,
-  stationTemplateToStationInfo,
-  warnTemplateKeysDeprecation
-} from './Helpers.js'
-import { IdTagsCache } from './IdTagsCache.js'
-import {
-  OCPP16IncomingRequestService,
-  OCPP16RequestService,
-  OCPP16ResponseService,
-  OCPP20IncomingRequestService,
-  OCPP20RequestService,
-  OCPP20ResponseService,
-  type OCPPIncomingRequestService,
-  type OCPPRequestService,
-  buildMeterValue,
-  buildTransactionEndMeterValue,
-  getMessageTypeString,
-  sendAndSetConnectorStatus
-} from './ocpp/index.js'
-import { SharedLRUCache } from './SharedLRUCache.js'
 import { BaseError, OCPPError } from '../exception/index.js'
 import { PerformanceStatistics } from '../performance/index.js'
 import {
@@ -113,17 +62,14 @@ import {
   SupervisionUrlDistribution,
   SupportedFeatureProfiles,
   type Voltage,
-  type WSError,
   WebSocketCloseEventStatusCode,
+  type WSError,
   type WsOptions
 } from '../types/index.js'
 import {
   ACElectricUtils,
   AsyncLock,
   AsyncLockType,
-  Configuration,
-  Constants,
-  DCElectricUtils,
   buildAddedMessage,
   buildChargingStationAutomaticTransactionGeneratorConfiguration,
   buildConnectorsStatus,
@@ -133,26 +79,81 @@ import {
   buildStoppedMessage,
   buildUpdatedMessage,
   clone,
+  Configuration,
+  Constants,
   convertToBoolean,
   convertToDate,
   convertToInt,
+  DCElectricUtils,
   exponentialDelay,
   formatDurationMilliSeconds,
   formatDurationSeconds,
-  getRandomInteger,
   getWebSocketCloseEventStatusString,
   handleFileException,
   isNotEmptyArray,
   isNotEmptyString,
-  logPrefix,
   logger,
+  logPrefix,
   min,
-  once,
   roundTo,
   secureRandom,
   sleep,
   watchJsonFile
 } from '../utils/index.js'
+import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js'
+import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel.js'
+import {
+  addConfigurationKey,
+  deleteConfigurationKey,
+  getConfigurationKey,
+  setConfigurationKeyValue
+} from './ConfigurationKeyUtils.js'
+import {
+  buildConnectorsMap,
+  buildTemplateName,
+  checkChargingStation,
+  checkConfiguration,
+  checkConnectorsConfiguration,
+  checkStationInfoConnectorStatus,
+  checkTemplate,
+  createBootNotificationRequest,
+  createSerialNumber,
+  getAmperageLimitationUnitDivider,
+  getBootConnectorStatus,
+  getChargingStationChargingProfilesLimit,
+  getChargingStationId,
+  getConnectorChargingProfilesLimit,
+  getDefaultVoltageOut,
+  getHashId,
+  getIdTagsFile,
+  getMaxNumberOfEvses,
+  getNumberOfReservableConnectors,
+  getPhaseRotationValue,
+  hasFeatureProfile,
+  hasReservationExpired,
+  initializeConnectorsMapStatus,
+  prepareConnectorStatus,
+  propagateSerialNumber,
+  setChargingStationOptions,
+  stationTemplateToStationInfo,
+  warnTemplateKeysDeprecation
+} from './Helpers.js'
+import { IdTagsCache } from './IdTagsCache.js'
+import {
+  buildMeterValue,
+  buildTransactionEndMeterValue,
+  getMessageTypeString,
+  OCPP16IncomingRequestService,
+  OCPP16RequestService,
+  OCPP16ResponseService,
+  OCPP20IncomingRequestService,
+  OCPP20RequestService,
+  OCPP20ResponseService,
+  type OCPPIncomingRequestService,
+  type OCPPRequestService,
+  sendAndSetConnectorStatus
+} from './ocpp/index.js'
+import { SharedLRUCache } from './SharedLRUCache.js'
 
 export class ChargingStation extends EventEmitter {
   public readonly index: number
@@ -182,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
@@ -197,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
@@ -226,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 => {
+      ).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 {
@@ -262,14 +266,17 @@ export class ChargingStation extends EventEmitter {
   }
 
   public get wsConnectionUrl (): URL {
+    const wsConnectionBaseUrlStr = `${
+      this.stationInfo?.supervisionUrlOcppConfiguration === true &&
+      isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
+      isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
+        ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
+        : this.configuredSupervisionUrl.href
+    }`
     return new URL(
-      `${
-        this.stationInfo?.supervisionUrlOcppConfiguration === true &&
-        isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
-        isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
-          ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
-          : this.configuredSupervisionUrl.href
-      }/${this.stationInfo?.chargingStationId}`
+      `${wsConnectionBaseUrlStr}${
+        !wsConnectionBaseUrlStr.endsWith('/') ? '/' : ''
+      }${this.stationInfo?.chargingStationId}`
     )
   }
 
@@ -287,7 +294,7 @@ export class ChargingStation extends EventEmitter {
         readFileSync(this.templateFile, 'utf8')
       ) as ChargingStationTemplate
     } catch {
-      stationTemplate = undefined
+      // Ignore
     }
     return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
   }
@@ -385,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(),
@@ -408,17 +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) ? Infinity : connectorMaximumPower,
+      isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower,
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      isNaN(connectorAmperageLimitationPowerLimit!)
-        ? Infinity
+      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!) ? Infinity : connectorChargingProfilesPowerLimit!
+      isNaN(connectorChargingProfilesLimit!)
+        ? Number.POSITIVE_INFINITY
+        : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        connectorChargingProfilesLimit!
     )
   }
 
@@ -534,15 +549,6 @@ export class ChargingStation extends EventEmitter {
     return Constants.DEFAULT_HEARTBEAT_INTERVAL
   }
 
-  public setSupervisionUrls (urls: string | string[], saveStationInfo = true): void {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    this.stationInfo!.supervisionUrls = urls
-    if (saveStationInfo) {
-      this.saveStationInfo()
-    }
-    this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
-  }
-
   public setSupervisionUrl (url: string): void {
     if (
       this.stationInfo?.supervisionUrlOcppConfiguration === true &&
@@ -550,36 +556,40 @@ export class ChargingStation extends EventEmitter {
     ) {
       setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
     } else {
-      this.setSupervisionUrls(url)
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      this.stationInfo!.supervisionUrls = url
+      this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
+      this.saveStationInfo()
     }
   }
 
   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)
-          .catch(error => {
+          .catch((error: unknown) => {
             logger.error(
               `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
               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`
       )
     }
   }
@@ -644,7 +654,7 @@ export class ChargingStation extends EventEmitter {
             meterValue: [meterValue]
           }
         )
-          .catch(error => {
+          .catch((error: unknown) => {
             logger.error(
               `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
               error
@@ -667,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)
   }
@@ -848,7 +863,7 @@ export class ChargingStation extends EventEmitter {
     this.wsConnection.on('close', this.onClose.bind(this))
     // Handle WebSocket open
     this.wsConnection.on('open', () => {
-      this.onOpen().catch(error =>
+      this.onOpen().catch((error: unknown) =>
         logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
       )
     })
@@ -950,7 +965,7 @@ export class ChargingStation extends EventEmitter {
       )
     }
     return await this.ocppRequestService.requestHandler<
-    StopTransactionRequest,
+    Partial<StopTransactionRequest>,
     StopTransactionResponse
     >(this, RequestCommand.STOP_TRANSACTION, {
       transactionId,
@@ -1161,7 +1176,7 @@ export class ChargingStation extends EventEmitter {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const stationTemplate = this.getTemplateFromFile()!
     checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
-    const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this)
+    const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation)
     warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
     if (stationTemplate.Connectors != null) {
       checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
@@ -1169,7 +1184,7 @@ export class ChargingStation extends EventEmitter {
     const stationInfo = stationTemplateToStationInfo(stationTemplate)
     stationInfo.hashId = getHashId(this.index, stationTemplate)
     stationInfo.templateIndex = this.index
-    stationInfo.templateName = parse(this.templateFile).name
+    stationInfo.templateName = buildTemplateName(this.templateFile)
     stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
     createSerialNumber(stationTemplate, stationInfo)
     stationInfo.voltageOut = this.getVoltageOut(stationInfo)
@@ -1198,15 +1213,6 @@ export class ChargingStation extends EventEmitter {
         } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
       )
     }
-    stationInfo.firmwareUpgrade = merge(
-      {
-        versionUpgrade: {
-          step: 1
-        },
-        reset: true
-      },
-      stationTemplate.firmwareUpgrade ?? {}
-    )
     if (stationTemplate.resetTime != null) {
       stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
     }
@@ -1229,7 +1235,7 @@ export class ChargingStation extends EventEmitter {
         }
         // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
         if (stationInfo.templateName == null) {
-          stationInfo.templateName = parse(this.templateFile).name
+          stationInfo.templateName = buildTemplateName(this.templateFile)
         }
       }
     }
@@ -1243,6 +1249,7 @@ export class ChargingStation extends EventEmitter {
     const stationInfoFromFile = this.getStationInfoFromFile(
       stationInfoFromTemplate.stationInfoPersistentConfiguration
     )
+    let stationInfo: ChargingStationInfo
     // Priority:
     // 1. charging station info from template
     // 2. charging station info from configuration file
@@ -1250,21 +1257,14 @@ export class ChargingStation extends EventEmitter {
       stationInfoFromFile != null &&
       stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
     ) {
-      return setChargingStationOptions(
-        this,
-        { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromFile },
-        options
-      )
+      stationInfo = stationInfoFromFile
+    } else {
+      stationInfo = stationInfoFromTemplate
+      stationInfoFromFile != null &&
+        propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile, stationInfo)
     }
-    stationInfoFromFile != null &&
-      propagateSerialNumber(
-        this.getTemplateFromFile(),
-        stationInfoFromFile,
-        stationInfoFromTemplate
-      )
     return setChargingStationOptions(
-      this,
-      { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromTemplate },
+      mergeDeepRight(Constants.DEFAULT_STATION_INFO, stationInfo),
       options
     )
   }
@@ -1380,7 +1380,9 @@ export class ChargingStation extends EventEmitter {
       addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
     }
     if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
-      addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false })
+      addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', {
+        visible: false
+      })
     }
     if (
       this.stationInfo?.supervisionUrlOcppConfiguration === true &&
@@ -1398,7 +1400,9 @@ export class ChargingStation extends EventEmitter {
       isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
       getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
     ) {
-      deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false })
+      deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, {
+        save: false
+      })
     }
     if (
       isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
@@ -1480,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()) {
@@ -1492,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)
             ])
           )
         })
@@ -1562,7 +1569,7 @@ export class ChargingStation extends EventEmitter {
             }
             const templateConnectorId =
               connectorId > 0 && stationTemplate.randomConnectors === true
-                ? getRandomInteger(templateMaxAvailableConnectors, 1)
+                ? randomInt(1, templateMaxAvailableConnectors)
                 : connectorId
             const connectorStatus = stationTemplate.Connectors[templateConnectorId]
             checkStationInfoConnectorStatus(
@@ -1730,7 +1737,7 @@ export class ChargingStation extends EventEmitter {
         } else {
           delete configurationData.configurationKey
         }
-        configurationData = merge(
+        configurationData = mergeDeepRight(
           configurationData,
           buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
         )
@@ -1757,7 +1764,9 @@ export class ChargingStation extends EventEmitter {
               ...(this.connectors.size > 0 && {
                 connectorsStatus: configurationData.connectorsStatus
               }),
-              ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus })
+              ...(this.evses.size > 0 && {
+                evsesStatus: configurationData.evsesStatus
+              })
             } satisfies ChargingStationConfiguration)
           )
           .digest('hex')
@@ -1775,7 +1784,7 @@ export class ChargingStation extends EventEmitter {
             this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
             this.sharedLRUCache.setChargingStationConfiguration(configurationData)
             this.configurationFileHash = configurationHash
-          }).catch(error => {
+          }).catch((error: unknown) => {
             handleFileException(
               this.configurationFile,
               FileType.ChargingStationConfiguration,
@@ -1832,26 +1841,27 @@ 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 ${this.wsConnectionUrl.href} succeeded`
+        `${this.logPrefix()} Connection to OCPP server through ${
+          this.wsConnectionUrl.href
+        } succeeded`
       )
       let registrationRetryCount = 0
       if (!this.isRegistered()) {
         // Send BootNotification
         do {
-          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(
@@ -1868,22 +1878,13 @@ export class ChargingStation extends EventEmitter {
             this.stationInfo?.registrationMaxRetries === -1)
         )
       }
-      if (this.isRegistered()) {
-        this.emit(ChargingStationEvents.registered)
-        if (this.inAcceptedState()) {
-          this.emit(ChargingStationEvents.accepted)
-        }
-      } else {
-        if (this.inRejectedState()) {
-          this.emit(ChargingStationEvents.rejected)
-        }
+      if (!this.isRegistered()) {
         logger.error(
           `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${
             this.stationInfo?.registrationMaxRetries
           })`
         )
       }
-      this.wsConnectionRetryCount = 0
       this.emit(ChargingStationEvents.updated)
     } else {
       logger.warn(
@@ -1894,6 +1895,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:
@@ -1913,12 +1915,15 @@ 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: unknown) =>
+              logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
+            )
         break
     }
-    this.emit(ChargingStationEvents.updated)
   }
 
   private getCachedRequest (
@@ -1931,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,
@@ -1941,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)
     }
@@ -1965,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
       )
@@ -1990,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 }
       )
@@ -2075,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
         )
       }
@@ -2088,7 +2101,9 @@ export class ChargingStation extends EventEmitter {
           // eslint-disable-next-line @typescript-eslint/no-base-to-string
         }' message '${data.toString()}'${
           this.requests.has(messageId)
-            ? ` matching cached request '${JSON.stringify(this.getCachedRequest(messageType, messageId))}'`
+            ? ` matching cached request '${JSON.stringify(
+                this.getCachedRequest(messageType, messageId)
+              )}'`
             : ''
         } processing error:`,
         error
@@ -2230,29 +2245,40 @@ export class ChargingStation extends EventEmitter {
       })
     }
     // Start WebSocket ping
-    this.startWebSocketPing()
+    if (this.wsPingSetInterval == null) {
+      this.startWebSocketPing()
+    }
     // Start heartbeat
-    this.startHeartbeat()
+    if (this.heartbeatSetInterval == null) {
+      this.startHeartbeat()
+    }
     // Initialize connectors status
     if (this.hasEvses) {
       for (const [evseId, evseStatus] of this.evses) {
         if (evseId > 0) {
           for (const [connectorId, connectorStatus] of evseStatus.connectors) {
-            const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus)
-            await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId)
+            await sendAndSetConnectorStatus(
+              this,
+              connectorId,
+              getBootConnectorStatus(this, connectorId, connectorStatus),
+              evseId
+            )
           }
         }
       }
     } else {
       for (const connectorId of this.connectors.keys()) {
         if (connectorId > 0) {
-          const connectorBootStatus = getBootConnectorStatus(
+          await sendAndSetConnectorStatus(
             this,
             connectorId,
-            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-            this.getConnectorStatus(connectorId)!
+            getBootConnectorStatus(
+              this,
+              connectorId,
+              // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+              this.getConnectorStatus(connectorId)!
+            )
           )
-          await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus)
         }
       }
     }
@@ -2315,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()) {
@@ -2369,11 +2396,11 @@ export class ChargingStation extends EventEmitter {
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
             Configuration.getSupervisionUrlDistribution()!
           ) &&
-            logger.error(
+            logger.warn(
               // eslint-disable-next-line @typescript-eslint/no-base-to-string
-              `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
+              `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
                 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
-              }`
+              }'`
             )
           configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
           break
@@ -2411,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
@@ -2438,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()})`
       )
     }
   }