refactor: cleanup loops over object keys
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 35d40ecec3bc14dba84bb1c2133eec747bff5a19..1494cc56365d983ec779c50dbd858af18cad739f 100644 (file)
@@ -1,18 +1,47 @@
 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
-import crypto from 'node:crypto';
-import fs from 'node:fs';
-import path from 'node:path';
+import { createHash } from 'node:crypto';
+import {
+  type FSWatcher,
+  closeSync,
+  existsSync,
+  mkdirSync,
+  openSync,
+  readFileSync,
+  writeFileSync,
+} from 'node:fs';
+import { dirname, join } from 'node:path';
 import { URL } from 'node:url';
 import { parentPort } from 'node:worker_threads';
 
 import merge from 'just-merge';
-import WebSocket, { type RawData } from 'ws';
+import { type RawData, WebSocket } from 'ws';
 
 import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator';
 import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel';
 import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils';
-import { ChargingStationUtils } from './ChargingStationUtils';
+import {
+  buildConnectorsMap,
+  checkConnectorsConfiguration,
+  checkStationInfoConnectorStatus,
+  checkTemplate,
+  countReservableConnectors,
+  createBootNotificationRequest,
+  createSerialNumber,
+  getAmperageLimitationUnitDivider,
+  getBootConnectorStatus,
+  getChargingStationConnectorChargingProfilesPowerLimit,
+  getChargingStationId,
+  getDefaultVoltageOut,
+  getHashId,
+  getIdTagsFile,
+  getMaxNumberOfEvses,
+  getPhaseRotationValue,
+  initializeConnectorsMapStatus,
+  propagateSerialNumber,
+  stationTemplateToStationInfo,
+  warnTemplateKeysDeprecation,
+} from './ChargingStationUtils';
 import { IdTagsCache } from './IdTagsCache';
 import {
   OCPP16IncomingRequestService,
@@ -67,8 +96,12 @@ import {
   PowerUnits,
   RegistrationStatusEnumType,
   RequestCommand,
+  type Reservation,
+  ReservationFilterKey,
+  ReservationTerminationReason,
   type Response,
   StandardParametersKey,
+  type Status,
   type StatusNotificationRequest,
   type StatusNotificationResponse,
   StopTransactionReason,
@@ -88,14 +121,31 @@ import {
   Configuration,
   Constants,
   DCElectricUtils,
-  ErrorUtils,
-  FileUtils,
-  MessageChannelUtils,
-  Utils,
   buildChargingStationAutomaticTransactionGeneratorConfiguration,
   buildConnectorsStatus,
   buildEvsesStatus,
+  buildStartedMessage,
+  buildStoppedMessage,
+  buildUpdatedMessage,
+  cloneObject,
+  convertToBoolean,
+  convertToInt,
+  exponentialDelay,
+  formatDurationMilliSeconds,
+  formatDurationSeconds,
+  getRandomInteger,
+  getWebSocketCloseEventStatusString,
+  handleFileException,
+  isNotEmptyArray,
+  isNotEmptyString,
+  isNullOrUndefined,
+  isUndefined,
+  logPrefix,
   logger,
+  roundTo,
+  secureRandom,
+  sleep,
+  watchJsonFile,
 } from '../utils';
 
 export class ChargingStation {
@@ -127,11 +177,12 @@ export class ChargingStation {
   private configuredSupervisionUrl!: URL;
   private wsConnectionRestarted: boolean;
   private autoReconnectRetryCount: number;
-  private templateFileWatcher!: fs.FSWatcher | undefined;
+  private templateFileWatcher!: FSWatcher | undefined;
   private templateFileHash!: string;
   private readonly sharedLRUCache: SharedLRUCache;
   private webSocketPingSetInterval!: NodeJS.Timeout;
   private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
+  private reservationExpirationSetInterval?: NodeJS.Timeout;
 
   constructor(index: number, templateFile: string) {
     this.started = false;
@@ -160,8 +211,8 @@ export class ChargingStation {
     return new URL(
       `${
         this.getSupervisionUrlOcppConfiguration() &&
-        Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
-        Utils.isNotEmptyString(
+        isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
+        isNotEmptyString(
           ChargingStationConfigurationUtils.getConfigurationKey(
             this,
             this.getSupervisionUrlOcppKey()
@@ -177,20 +228,18 @@ export class ChargingStation {
   }
 
   public logPrefix = (): string => {
-    return Utils.logPrefix(
+    return logPrefix(
       ` ${
-        (Utils.isNotEmptyString(this?.stationInfo?.chargingStationId)
+        (isNotEmptyString(this?.stationInfo?.chargingStationId)
           ? this?.stationInfo?.chargingStationId
-          : ChargingStationUtils.getChargingStationId(this.index, this.getTemplateFromFile())) ??
+          : getChargingStationId(this.index, this.getTemplateFromFile())) ??
         'Error at building log prefix'
       } |`
     );
   };
 
   public hasIdTags(): boolean {
-    return Utils.isNotEmptyArray(
-      this.idTagsCache.getIdTags(ChargingStationUtils.getIdTagsFile(this.stationInfo))
-    );
+    return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo)));
   }
 
   public getEnableStatistics(): boolean {
@@ -201,17 +250,11 @@ export class ChargingStation {
     return this.stationInfo.mustAuthorizeAtRemoteStart ?? true;
   }
 
-  public getPayloadSchemaValidation(): boolean {
-    return this.stationInfo.payloadSchemaValidation ?? true;
-  }
-
   public getNumberOfPhases(stationInfo?: ChargingStationInfo): number | undefined {
     const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo;
     switch (this.getCurrentOutType(stationInfo)) {
       case CurrentType.AC:
-        return !Utils.isUndefined(localStationInfo.numberOfPhases)
-          ? localStationInfo.numberOfPhases
-          : 3;
+        return !isUndefined(localStationInfo.numberOfPhases) ? localStationInfo.numberOfPhases : 3;
       case CurrentType.DC:
         return 0;
     }
@@ -226,7 +269,7 @@ export class ChargingStation {
   }
 
   public inUnknownState(): boolean {
-    return Utils.isNullOrUndefined(this?.bootNotificationResponse?.status);
+    return isNullOrUndefined(this?.bootNotificationResponse?.status);
   }
 
   public inPendingState(): boolean {
@@ -273,9 +316,6 @@ export class ChargingStation {
 
   public getNumberOfConnectors(): number {
     if (this.hasEvses) {
-      if (this.evses.size === 0) {
-        throw new BaseError('Evses not initialized, cannot get number of connectors');
-      }
       let numberOfConnectors = 0;
       for (const [evseId, evseStatus] of this.evses) {
         if (evseId > 0) {
@@ -284,16 +324,10 @@ export class ChargingStation {
       }
       return numberOfConnectors;
     }
-    if (this.connectors.size === 0) {
-      throw new BaseError('Connectors not initialized, cannot get number of connectors');
-    }
     return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size;
   }
 
   public getNumberOfEvses(): number {
-    if (this.evses.size === 0) {
-      throw new BaseError('Evses not initialized, cannot get number of evses');
-    }
     return this.evses.has(0) ? this.evses.size - 1 : this.evses.size;
   }
 
@@ -314,11 +348,11 @@ export class ChargingStation {
   }
 
   public getOcppStrictCompliance(): boolean {
-    return this.stationInfo?.ocppStrictCompliance ?? false;
+    return this.stationInfo?.ocppStrictCompliance ?? true;
   }
 
   public getVoltageOut(stationInfo?: ChargingStationInfo): number | undefined {
-    const defaultVoltageOut = ChargingStationUtils.getDefaultVoltageOut(
+    const defaultVoltageOut = getDefaultVoltageOut(
       this.getCurrentOutType(stationInfo),
       this.logPrefix(),
       this.templateFile
@@ -334,7 +368,7 @@ export class ChargingStation {
   public getConnectorMaximumAvailablePower(connectorId: number): number {
     let connectorAmperageLimitationPowerLimit: number;
     if (
-      !Utils.isNullOrUndefined(this.getAmperageLimitation()) &&
+      !isNullOrUndefined(this.getAmperageLimitation()) &&
       this.getAmperageLimitation() < this.stationInfo?.maximumAmperage
     ) {
       connectorAmperageLimitationPowerLimit =
@@ -350,7 +384,7 @@ export class ChargingStation {
     }
     const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
     const connectorChargingProfilesPowerLimit =
-      ChargingStationUtils.getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
+      getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
     return Math.min(
       isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
       isNaN(connectorAmperageLimitationPowerLimit)
@@ -371,10 +405,7 @@ export class ChargingStation {
       }
     } else {
       for (const connectorId of this.connectors.keys()) {
-        if (
-          connectorId > 0 &&
-          this.getConnectorStatus(connectorId)?.transactionId === transactionId
-        ) {
+        if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
           return this.getConnectorStatus(connectorId)?.transactionIdTag;
         }
       }
@@ -384,7 +415,10 @@ export class ChargingStation {
   public getNumberOfRunningTransactions(): number {
     let trxCount = 0;
     if (this.hasEvses) {
-      for (const evseStatus of this.evses.values()) {
+      for (const [evseId, evseStatus] of this.evses) {
+        if (evseId === 0) {
+          continue;
+        }
         for (const connectorStatus of evseStatus.connectors.values()) {
           if (connectorStatus.transactionStarted === true) {
             ++trxCount;
@@ -440,10 +474,7 @@ export class ChargingStation {
       }
     } else {
       for (const connectorId of this.connectors.keys()) {
-        if (
-          connectorId > 0 &&
-          this.getConnectorStatus(connectorId)?.transactionId === transactionId
-        ) {
+        if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
           return connectorId;
         }
       }
@@ -469,9 +500,7 @@ export class ChargingStation {
       this,
       StandardParametersKey.AuthorizeRemoteTxRequests
     );
-    return authorizeRemoteTxRequests
-      ? Utils.convertToBoolean(authorizeRemoteTxRequests.value)
-      : false;
+    return authorizeRemoteTxRequests ? convertToBoolean(authorizeRemoteTxRequests.value) : false;
   }
 
   public getLocalAuthListEnabled(): boolean {
@@ -479,7 +508,7 @@ export class ChargingStation {
       this,
       StandardParametersKey.LocalAuthListEnabled
     );
-    return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
+    return localAuthListEnabled ? convertToBoolean(localAuthListEnabled.value) : false;
   }
 
   public getHeartbeatInterval(): number {
@@ -488,14 +517,14 @@ export class ChargingStation {
       StandardParametersKey.HeartbeatInterval
     );
     if (HeartbeatInterval) {
-      return Utils.convertToInt(HeartbeatInterval.value) * 1000;
+      return convertToInt(HeartbeatInterval.value) * 1000;
     }
     const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
       this,
       StandardParametersKey.HeartBeatInterval
     );
     if (HeartBeatInterval) {
-      return Utils.convertToInt(HeartBeatInterval.value) * 1000;
+      return convertToInt(HeartBeatInterval.value) * 1000;
     }
     this.stationInfo?.autoRegister === false &&
       logger.warn(
@@ -509,7 +538,7 @@ export class ChargingStation {
   public setSupervisionUrl(url: string): void {
     if (
       this.getSupervisionUrlOcppConfiguration() &&
-      Utils.isNotEmptyString(this.getSupervisionUrlOcppKey())
+      isNotEmptyString(this.getSupervisionUrlOcppKey())
     ) {
       ChargingStationConfigurationUtils.setConfigurationKeyValue(
         this,
@@ -536,19 +565,20 @@ export class ChargingStation {
           });
       }, this.getHeartbeatInterval());
       logger.info(
-        `${this.logPrefix()} Heartbeat started every ${Utils.formatDurationMilliSeconds(
+        `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
           this.getHeartbeatInterval()
         )}`
       );
     } else if (this.heartbeatSetInterval) {
       logger.info(
-        `${this.logPrefix()} Heartbeat already started every ${Utils.formatDurationMilliSeconds(
+        `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
           this.getHeartbeatInterval()
         )}`
       );
     } else {
       logger.error(
-        `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
+        `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()},
+          not starting the heartbeat`
       );
     }
   }
@@ -576,21 +606,24 @@ export class ChargingStation {
     }
     if (!this.getConnectorStatus(connectorId)) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on non existing connector id ${connectorId.toString()}`
+        `${this.logPrefix()} Trying to start MeterValues on non existing connector id
+          ${connectorId.toString()}`
       );
       return;
     }
     if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
+        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
+          with no transaction started`
       );
       return;
     } else if (
       this.getConnectorStatus(connectorId)?.transactionStarted === true &&
-      Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
+      isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
     ) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
+        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
+          with no transaction id`
       );
       return;
     }
@@ -642,15 +675,18 @@ export class ChargingStation {
         if (this.getEnableStatistics() === true) {
           this.performanceStatistics?.start();
         }
+        if (this.hasFeatureProfile(SupportedFeatureProfiles.Reservation)) {
+          this.startReservationExpirationSetInterval();
+        }
         this.openWSConnection();
         // Monitor charging station template file
-        this.templateFileWatcher = FileUtils.watchJsonFile(
+        this.templateFileWatcher = watchJsonFile(
           this.templateFile,
           FileType.ChargingStationTemplate,
           this.logPrefix(),
           undefined,
           (event, filename): void => {
-            if (Utils.isNotEmptyString(filename) && event === 'change') {
+            if (isNotEmptyString(filename) && event === 'change') {
               try {
                 logger.debug(
                   `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
@@ -660,6 +696,7 @@ export class ChargingStation {
                 this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
                 // Initialize
                 this.initialize();
+                this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo));
                 // Restart the ATG
                 this.stopAutomaticTransactionGenerator();
                 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
@@ -681,7 +718,7 @@ export class ChargingStation {
           }
         );
         this.started = true;
-        parentPort?.postMessage(MessageChannelUtils.buildStartedMessage(this));
+        parentPort?.postMessage(buildStartedMessage(this));
         this.starting = false;
       } else {
         logger.warn(`${this.logPrefix()} Charging station is already starting...`);
@@ -706,7 +743,7 @@ export class ChargingStation {
         delete this.bootNotificationResponse;
         this.started = false;
         this.saveConfiguration();
-        parentPort?.postMessage(MessageChannelUtils.buildStoppedMessage(this));
+        parentPort?.postMessage(buildStoppedMessage(this));
         this.stopping = false;
       } else {
         logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
@@ -718,7 +755,7 @@ export class ChargingStation {
 
   public async reset(reason?: StopTransactionReason): Promise<void> {
     await this.stop(reason);
-    await Utils.sleep(this.stationInfo.resetTime);
+    await sleep(this.stationInfo.resetTime);
     this.initialize();
     this.start();
   }
@@ -747,17 +784,18 @@ export class ChargingStation {
       terminateOpened: false,
     }
   ): void {
-    options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
+    options = { handshakeTimeout: this.getConnectionTimeout() * 1000, ...options };
     params = { ...{ closeOpened: false, terminateOpened: false }, ...params };
     if (this.started === false && this.starting === false) {
       logger.warn(
-        `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station`
+        `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()}
+          on stopped charging station`
       );
       return;
     }
     if (
-      !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
-      !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
+      !isNullOrUndefined(this.stationInfo.supervisionUser) &&
+      !isNullOrUndefined(this.stationInfo.supervisionPassword)
     ) {
       options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
     }
@@ -770,7 +808,8 @@ export class ChargingStation {
 
     if (this.isWebSocketConnectionOpened() === true) {
       logger.warn(
-        `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
+        `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()}
+          is already opened`
       );
       return;
     }
@@ -818,37 +857,54 @@ export class ChargingStation {
   public getAutomaticTransactionGeneratorConfiguration():
     | AutomaticTransactionGeneratorConfiguration
     | undefined {
+    let automaticTransactionGeneratorConfiguration:
+      | AutomaticTransactionGeneratorConfiguration
+      | undefined;
     const automaticTransactionGeneratorConfigurationFromFile =
       this.getConfigurationFromFile()?.automaticTransactionGenerator;
-    if (automaticTransactionGeneratorConfigurationFromFile) {
-      return automaticTransactionGeneratorConfigurationFromFile;
+    if (
+      this.getAutomaticTransactionGeneratorPersistentConfiguration() &&
+      automaticTransactionGeneratorConfigurationFromFile
+    ) {
+      automaticTransactionGeneratorConfiguration =
+        automaticTransactionGeneratorConfigurationFromFile;
+    } else {
+      automaticTransactionGeneratorConfiguration =
+        this.getTemplateFromFile()?.AutomaticTransactionGenerator;
     }
-    return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
+    return {
+      ...Constants.DEFAULT_ATG_CONFIGURATION,
+      ...automaticTransactionGeneratorConfiguration,
+    };
+  }
+
+  public getAutomaticTransactionGeneratorStatuses(): Status[] | undefined {
+    return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses;
   }
 
   public startAutomaticTransactionGenerator(connectorIds?: number[]): void {
     this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this);
-    if (Utils.isNotEmptyArray(connectorIds)) {
+    if (isNotEmptyArray(connectorIds)) {
       for (const connectorId of connectorIds) {
         this.automaticTransactionGenerator?.startConnector(connectorId);
       }
     } else {
       this.automaticTransactionGenerator?.start();
     }
-    this.saveChargingStationAutomaticTransactionGeneratorConfiguration();
-    parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
+    this.saveAutomaticTransactionGeneratorConfiguration();
+    parentPort?.postMessage(buildUpdatedMessage(this));
   }
 
   public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
-    if (Utils.isNotEmptyArray(connectorIds)) {
+    if (isNotEmptyArray(connectorIds)) {
       for (const connectorId of connectorIds) {
         this.automaticTransactionGenerator?.stopConnector(connectorId);
       }
     } else {
       this.automaticTransactionGenerator?.stop();
     }
-    this.saveChargingStationAutomaticTransactionGeneratorConfiguration();
-    parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
+    this.saveAutomaticTransactionGeneratorConfiguration();
+    parentPort?.postMessage(buildUpdatedMessage(this));
   }
 
   public async stopTransactionOnConnector(
@@ -888,6 +944,172 @@ export class ChargingStation {
     );
   }
 
+  public getReservationOnConnectorId0Enabled(): boolean {
+    return convertToBoolean(
+      ChargingStationConfigurationUtils.getConfigurationKey(
+        this,
+        StandardParametersKey.ReserveConnectorZeroSupported
+      ).value
+    );
+  }
+
+  public async addReservation(reservation: Reservation): Promise<void> {
+    const [exists, reservationFound] = this.doesReservationExists(reservation);
+    if (exists) {
+      await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING);
+    }
+    this.getConnectorStatus(reservation.connectorId).reservation = reservation;
+    await OCPPServiceUtils.sendAndSetConnectorStatus(
+      this,
+      reservation.connectorId,
+      ConnectorStatusEnum.Reserved,
+      null,
+      { send: reservation.connectorId !== 0 }
+    );
+  }
+
+  public async removeReservation(
+    reservation: Reservation,
+    reason?: ReservationTerminationReason
+  ): Promise<void> {
+    const connector = this.getConnectorStatus(reservation.connectorId);
+    switch (reason) {
+      case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
+        delete connector.reservation;
+        break;
+      case ReservationTerminationReason.TRANSACTION_STARTED:
+        delete connector.reservation;
+        break;
+      case ReservationTerminationReason.RESERVATION_CANCELED:
+      case ReservationTerminationReason.REPLACE_EXISTING:
+      case ReservationTerminationReason.EXPIRED:
+        await OCPPServiceUtils.sendAndSetConnectorStatus(
+          this,
+          reservation.connectorId,
+          ConnectorStatusEnum.Available,
+          null,
+          { send: reservation.connectorId !== 0 }
+        );
+        delete connector.reservation;
+        break;
+      default:
+        break;
+    }
+  }
+
+  public getReservationBy(
+    filterKey: ReservationFilterKey,
+    value: number | string
+  ): Reservation | undefined {
+    if (this.hasEvses) {
+      for (const evseStatus of this.evses.values()) {
+        for (const connectorStatus of evseStatus.connectors.values()) {
+          if (connectorStatus?.reservation?.[filterKey] === value) {
+            return connectorStatus.reservation;
+          }
+        }
+      }
+    } else {
+      for (const connectorStatus of this.connectors.values()) {
+        if (connectorStatus?.reservation?.[filterKey] === value) {
+          return connectorStatus.reservation;
+        }
+      }
+    }
+  }
+
+  public doesReservationExists(reservation: Partial<Reservation>): [boolean, Reservation] {
+    const foundReservation = this.getReservationBy(
+      ReservationFilterKey.RESERVATION_ID,
+      reservation?.id
+    );
+    return isUndefined(foundReservation) ? [false, null] : [true, foundReservation];
+  }
+
+  public startReservationExpirationSetInterval(customInterval?: number): void {
+    const interval =
+      customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL;
+    logger.info(
+      `${this.logPrefix()} Reservation expiration date interval is set to ${interval}
+        and starts on charging station now`
+    );
+    // eslint-disable-next-line @typescript-eslint/no-misused-promises
+    this.reservationExpirationSetInterval = setInterval(async (): Promise<void> => {
+      const now = new Date();
+      if (this.hasEvses) {
+        for (const evseStatus of this.evses.values()) {
+          for (const connectorStatus of evseStatus.connectors.values()) {
+            if (connectorStatus?.reservation?.expiryDate < now) {
+              await this.removeReservation(
+                connectorStatus.reservation,
+                ReservationTerminationReason.EXPIRED
+              );
+            }
+          }
+        }
+      } else {
+        for (const connectorStatus of this.connectors.values()) {
+          if (connectorStatus?.reservation?.expiryDate < now) {
+            await this.removeReservation(
+              connectorStatus.reservation,
+              ReservationTerminationReason.EXPIRED
+            );
+          }
+        }
+      }
+    }, interval);
+  }
+
+  public restartReservationExpiryDateSetInterval(): void {
+    this.stopReservationExpirationSetInterval();
+    this.startReservationExpirationSetInterval();
+  }
+
+  public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
+    return this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId)?.idTag === idTag;
+  }
+
+  public isConnectorReservable(
+    reservationId: number,
+    idTag?: string,
+    connectorId?: number
+  ): boolean {
+    const [alreadyExists] = this.doesReservationExists({ id: reservationId });
+    if (alreadyExists) {
+      return alreadyExists;
+    }
+    const userReservedAlready = isUndefined(
+      this.getReservationBy(ReservationFilterKey.ID_TAG, idTag)
+    )
+      ? false
+      : true;
+    const notConnectorZero = isUndefined(connectorId) ? true : connectorId > 0;
+    const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
+    return !alreadyExists && !userReservedAlready && notConnectorZero && freeConnectorsAvailable;
+  }
+
+  private getNumberOfReservableConnectors(): number {
+    let reservableConnectors = 0;
+    if (this.hasEvses) {
+      for (const evseStatus of this.evses.values()) {
+        reservableConnectors += countReservableConnectors(evseStatus.connectors);
+      }
+    } else {
+      reservableConnectors = countReservableConnectors(this.connectors);
+    }
+    return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
+  }
+
+  private getNumberOfReservationsOnConnectorZero(): number {
+    let numberOfReservations = 0;
+    if (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation) {
+      ++numberOfReservations;
+    } else if (this.connectors.get(0)?.reservation) {
+      ++numberOfReservations;
+    }
+    return numberOfReservations;
+  }
+
   private flushMessageBuffer(): void {
     if (this.messageBuffer.size > 0) {
       for (const message of this.messageBuffer.values()) {
@@ -915,6 +1137,12 @@ export class ChargingStation {
     return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
   }
 
+  private stopReservationExpirationSetInterval(): void {
+    if (this.reservationExpirationSetInterval) {
+      clearInterval(this.reservationExpirationSetInterval);
+    }
+  }
+
   private getSupervisionUrlOcppKey(): string {
     return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
   }
@@ -927,19 +1155,16 @@ export class ChargingStation {
       } else {
         const measureId = `${FileType.ChargingStationTemplate} read`;
         const beginId = PerformanceStatistics.beginMeasure(measureId);
-        template = JSON.parse(
-          fs.readFileSync(this.templateFile, 'utf8')
-        ) as ChargingStationTemplate;
+        template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate;
         PerformanceStatistics.endMeasure(measureId, beginId);
-        template.templateHash = crypto
-          .createHash(Constants.DEFAULT_HASH_ALGORITHM)
+        template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
           .update(JSON.stringify(template))
           .digest('hex');
         this.sharedLRUCache.setChargingStationTemplate(template);
         this.templateFileHash = template.templateHash;
       }
     } catch (error) {
-      ErrorUtils.handleFileException(
+      handleFileException(
         this.templateFile,
         FileType.ChargingStationTemplate,
         error as NodeJS.ErrnoException,
@@ -951,31 +1176,19 @@ export class ChargingStation {
 
   private getStationInfoFromTemplate(): ChargingStationInfo {
     const stationTemplate: ChargingStationTemplate | undefined = this.getTemplateFromFile();
-    ChargingStationUtils.checkTemplateFile(stationTemplate, this.logPrefix(), this.templateFile);
-    ChargingStationUtils.warnTemplateKeysDeprecation(
-      stationTemplate,
-      this.logPrefix(),
-      this.templateFile
-    );
+    checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
+    warnTemplateKeysDeprecation(stationTemplate, this.logPrefix(), this.templateFile);
     if (stationTemplate?.Connectors) {
-      ChargingStationUtils.checkConnectorsConfiguration(
-        stationTemplate,
-        this.logPrefix(),
-        this.templateFile
-      );
+      checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
     }
-    const stationInfo: ChargingStationInfo =
-      ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
-    stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
-    stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
-      this.index,
-      stationTemplate
-    );
+    const stationInfo: ChargingStationInfo = stationTemplateToStationInfo(stationTemplate);
+    stationInfo.hashId = getHashId(this.index, stationTemplate);
+    stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate);
     stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
-    ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
-    if (Utils.isNotEmptyArray(stationTemplate?.power)) {
+    createSerialNumber(stationTemplate, stationInfo);
+    if (isNotEmptyArray(stationTemplate?.power)) {
       stationTemplate.power = stationTemplate.power as number[];
-      const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
+      const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length);
       stationInfo.maximumPower =
         stationTemplate?.powerUnit === PowerUnits.KILO_WATT
           ? stationTemplate.power[powerArrayRandomIndex] * 1000
@@ -990,7 +1203,7 @@ export class ChargingStation {
     stationInfo.firmwareVersionPattern =
       stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
     if (
-      Utils.isNotEmptyString(stationInfo.firmwareVersion) &&
+      isNotEmptyString(stationInfo.firmwareVersion) &&
       new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false
     ) {
       logger.warn(
@@ -1008,7 +1221,7 @@ export class ChargingStation {
       },
       stationTemplate?.firmwareUpgrade ?? {}
     );
-    stationInfo.resetTime = !Utils.isNullOrUndefined(stationTemplate?.resetTime)
+    stationInfo.resetTime = !isNullOrUndefined(stationTemplate?.resetTime)
       ? stationTemplate.resetTime * 1000
       : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
     stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
@@ -1036,7 +1249,7 @@ export class ChargingStation {
       return stationInfoFromFile;
     }
     stationInfoFromFile &&
-      ChargingStationUtils.propagateSerialNumber(
+      propagateSerialNumber(
         this.getTemplateFromFile(),
         stationInfoFromFile,
         stationInfoFromTemplate
@@ -1058,23 +1271,27 @@ export class ChargingStation {
     return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
   }
 
+  private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
+    return this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ?? true;
+  }
+
   private handleUnsupportedVersion(version: OCPPVersion) {
-    const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
+    const errorMsg = `Unsupported protocol version '${version}' configured
+      in template file ${this.templateFile}`;
     logger.error(`${this.logPrefix()} ${errorMsg}`);
     throw new BaseError(errorMsg);
   }
 
   private initialize(): void {
     const stationTemplate = this.getTemplateFromFile();
-    ChargingStationUtils.checkTemplateFile(stationTemplate, this.logPrefix(), this.templateFile);
-    this.configurationFile = path.join(
-      path.dirname(this.templateFile.replace('station-templates', 'configurations')),
-      `${ChargingStationUtils.getHashId(this.index, stationTemplate)}.json`
+    checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
+    this.configurationFile = join(
+      dirname(this.templateFile.replace('station-templates', 'configurations')),
+      `${getHashId(this.index, stationTemplate)}.json`
     );
     const chargingStationConfiguration = this.getConfigurationFromFile();
-    const featureFlag = false;
     if (
-      featureFlag &&
+      chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
       (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus)
     ) {
       this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
@@ -1084,8 +1301,8 @@ export class ChargingStation {
     this.stationInfo = this.getStationInfo();
     if (
       this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
-      Utils.isNotEmptyString(this.stationInfo.firmwareVersion) &&
-      Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern)
+      isNotEmptyString(this.stationInfo.firmwareVersion) &&
+      isNotEmptyString(this.stationInfo.firmwareVersionPattern)
     ) {
       const patternGroup: number | undefined =
         this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
@@ -1095,7 +1312,7 @@ export class ChargingStation {
         ?.slice(1, patternGroup + 1);
       const patchLevelIndex = match.length - 1;
       match[patchLevelIndex] = (
-        Utils.convertToInt(match[patchLevelIndex]) +
+        convertToInt(match[patchLevelIndex]) +
         this.stationInfo.firmwareUpgrade?.versionUpgrade?.step
       ).toString();
       this.stationInfo.firmwareVersion = match?.join('.');
@@ -1109,9 +1326,7 @@ export class ChargingStation {
         this.configuredSupervisionUrl
       );
     }
-    this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
-      this.stationInfo
-    );
+    this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo);
     this.powerDivider = this.getPowerDivider();
     // OCPP configuration
     this.ocppConfiguration = this.getOcppConfiguration();
@@ -1178,7 +1393,7 @@ export class ChargingStation {
     }
     if (
       this.getSupervisionUrlOcppConfiguration() &&
-      Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
+      isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
       !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
     ) {
       ChargingStationConfigurationUtils.addConfigurationKey(
@@ -1189,7 +1404,7 @@ export class ChargingStation {
       );
     } else if (
       !this.getSupervisionUrlOcppConfiguration() &&
-      Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
+      isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
       ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
     ) {
       ChargingStationConfigurationUtils.deleteConfigurationKey(
@@ -1199,7 +1414,7 @@ export class ChargingStation {
       );
     }
     if (
-      Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
+      isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
       !ChargingStationConfigurationUtils.getConfigurationKey(
         this,
         this.stationInfo.amperageLimitationOcppKey
@@ -1209,8 +1424,7 @@ export class ChargingStation {
         this,
         this.stationInfo.amperageLimitationOcppKey,
         (
-          this.stationInfo.maximumAmperage *
-          ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
+          this.stationInfo.maximumAmperage * getAmperageLimitationUnitDivider(this.stationInfo)
         ).toString()
       );
     }
@@ -1256,14 +1470,14 @@ export class ChargingStation {
         for (const evseStatus of this.evses.values()) {
           for (const connectorId of evseStatus.connectors.keys()) {
             connectorsPhaseRotation.push(
-              ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
+              getPhaseRotationValue(connectorId, this.getNumberOfPhases())
             );
           }
         }
       } else {
         for (const connectorId of this.connectors.keys()) {
           connectorsPhaseRotation.push(
-            ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases())
+            getPhaseRotationValue(connectorId, this.getNumberOfPhases())
           );
         }
       }
@@ -1319,11 +1533,11 @@ export class ChargingStation {
   private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void {
     if (configuration?.connectorsStatus && !configuration?.evsesStatus) {
       for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
-        this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
+        this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
       }
     } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) {
       for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
-        const evseStatus = Utils.cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
+        const evseStatus = cloneObject<EvseStatusConfiguration>(evseStatusConfiguration);
         delete evseStatus.connectorsStatus;
         this.evses.set(evseId, {
           ...(evseStatus as EvseStatus),
@@ -1377,13 +1591,8 @@ export class ChargingStation {
     }
     if (stationTemplate?.Connectors) {
       const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
-        ChargingStationUtils.checkConnectorsConfiguration(
-          stationTemplate,
-          this.logPrefix(),
-          this.templateFile
-        );
-      const connectorsConfigHash = crypto
-        .createHash(Constants.DEFAULT_HASH_ALGORITHM)
+        checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
+      const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
         .update(
           `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`
         )
@@ -1404,18 +1613,18 @@ export class ChargingStation {
             }
             const templateConnectorId =
               connectorId > 0 && stationTemplate?.randomConnectors
-                ? Utils.getRandomInteger(templateMaxAvailableConnectors, 1)
+                ? getRandomInteger(templateMaxAvailableConnectors, 1)
                 : connectorId;
             const connectorStatus = stationTemplate?.Connectors[templateConnectorId];
-            ChargingStationUtils.checkStationInfoConnectorStatus(
+            checkStationInfoConnectorStatus(
               templateConnectorId,
               connectorStatus,
               this.logPrefix(),
               this.templateFile
             );
-            this.connectors.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
+            this.connectors.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
           }
-          ChargingStationUtils.initializeConnectorsMapStatus(this.connectors, this.logPrefix());
+          initializeConnectorsMapStatus(this.connectors, this.logPrefix());
           this.saveConnectorsStatus();
         } else {
           logger.warn(
@@ -1455,31 +1664,27 @@ export class ChargingStation {
       );
     }
     if (stationTemplate?.Evses) {
-      const evsesConfigHash = crypto
-        .createHash(Constants.DEFAULT_HASH_ALGORITHM)
-        .update(`${JSON.stringify(stationTemplate?.Evses)}`)
+      const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+        .update(JSON.stringify(stationTemplate?.Evses))
         .digest('hex');
       const evsesConfigChanged =
         this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
       if (this.evses?.size === 0 || evsesConfigChanged) {
         evsesConfigChanged && this.evses.clear();
         this.evsesConfigurationHash = evsesConfigHash;
-        const templateMaxEvses = ChargingStationUtils.getMaxNumberOfEvses(stationTemplate?.Evses);
+        const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses);
         if (templateMaxEvses > 0) {
           for (const evse in stationTemplate.Evses) {
-            const evseId = Utils.convertToInt(evse);
+            const evseId = convertToInt(evse);
             this.evses.set(evseId, {
-              connectors: ChargingStationUtils.buildConnectorsMap(
+              connectors: buildConnectorsMap(
                 stationTemplate?.Evses[evse]?.Connectors,
                 this.logPrefix(),
                 this.templateFile
               ),
               availability: AvailabilityType.Operative,
             });
-            ChargingStationUtils.initializeConnectorsMapStatus(
-              this.evses.get(evseId)?.connectors,
-              this.logPrefix()
-            );
+            initializeConnectorsMapStatus(this.evses.get(evseId)?.connectors, this.logPrefix());
           }
           this.saveEvsesStatus();
         } else {
@@ -1501,7 +1706,7 @@ export class ChargingStation {
 
   private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
     let configuration: ChargingStationConfiguration | undefined;
-    if (Utils.isNotEmptyString(this.configurationFile) && fs.existsSync(this.configurationFile)) {
+    if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
       try {
         if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
           configuration = this.sharedLRUCache.getChargingStationConfiguration(
@@ -1511,14 +1716,14 @@ export class ChargingStation {
           const measureId = `${FileType.ChargingStationConfiguration} read`;
           const beginId = PerformanceStatistics.beginMeasure(measureId);
           configuration = JSON.parse(
-            fs.readFileSync(this.configurationFile, 'utf8')
+            readFileSync(this.configurationFile, 'utf8')
           ) as ChargingStationConfiguration;
           PerformanceStatistics.endMeasure(measureId, beginId);
           this.sharedLRUCache.setChargingStationConfiguration(configuration);
           this.configurationFileHash = configuration.configurationHash;
         }
       } catch (error) {
-        ErrorUtils.handleFileException(
+        handleFileException(
           this.configurationFile,
           FileType.ChargingStationConfiguration,
           error as NodeJS.ErrnoException,
@@ -1529,8 +1734,10 @@ export class ChargingStation {
     return configuration;
   }
 
-  private saveChargingStationAutomaticTransactionGeneratorConfiguration(): void {
-    this.saveConfiguration();
+  private saveAutomaticTransactionGeneratorConfiguration(): void {
+    if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
+      this.saveConfiguration();
+    }
   }
 
   private saveConnectorsStatus() {
@@ -1542,33 +1749,52 @@ export class ChargingStation {
   }
 
   private saveConfiguration(): void {
-    if (Utils.isNotEmptyString(this.configurationFile)) {
+    if (isNotEmptyString(this.configurationFile)) {
       try {
-        if (!fs.existsSync(path.dirname(this.configurationFile))) {
-          fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
+        if (!existsSync(dirname(this.configurationFile))) {
+          mkdirSync(dirname(this.configurationFile), { recursive: true });
         }
         let configurationData: ChargingStationConfiguration =
-          Utils.cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()) ?? {};
+          cloneObject<ChargingStationConfiguration>(this.getConfigurationFromFile()) ?? {};
         if (this.getStationInfoPersistentConfiguration() && this.stationInfo) {
           configurationData.stationInfo = this.stationInfo;
+        } else {
+          delete configurationData.stationInfo;
         }
         if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) {
           configurationData.configurationKey = this.ocppConfiguration.configurationKey;
+        } else {
+          delete configurationData.configurationKey;
         }
         configurationData = merge<ChargingStationConfiguration>(
           configurationData,
           buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
         );
+        if (
+          !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
+          !this.getAutomaticTransactionGeneratorConfiguration()
+        ) {
+          delete configurationData.automaticTransactionGenerator;
+        }
         if (this.connectors.size > 0) {
           configurationData.connectorsStatus = buildConnectorsStatus(this);
+        } else {
+          delete configurationData.connectorsStatus;
         }
         if (this.evses.size > 0) {
           configurationData.evsesStatus = buildEvsesStatus(this);
+        } else {
+          delete configurationData.evsesStatus;
         }
         delete configurationData.configurationHash;
-        const configurationHash = crypto
-          .createHash(Constants.DEFAULT_HASH_ALGORITHM)
-          .update(JSON.stringify(configurationData))
+        const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+          .update(
+            JSON.stringify({
+              stationInfo: configurationData.stationInfo,
+              configurationKey: configurationData.configurationKey,
+              automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
+            } as ChargingStationConfiguration)
+          )
           .digest('hex');
         if (this.configurationFileHash !== configurationHash) {
           AsyncLock.acquire(AsyncLockType.configuration)
@@ -1576,16 +1802,16 @@ export class ChargingStation {
               configurationData.configurationHash = configurationHash;
               const measureId = `${FileType.ChargingStationConfiguration} write`;
               const beginId = PerformanceStatistics.beginMeasure(measureId);
-              const fileDescriptor = fs.openSync(this.configurationFile, 'w');
-              fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
-              fs.closeSync(fileDescriptor);
+              const fileDescriptor = openSync(this.configurationFile, 'w');
+              writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
+              closeSync(fileDescriptor);
               PerformanceStatistics.endMeasure(measureId, beginId);
               this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
               this.sharedLRUCache.setChargingStationConfiguration(configurationData);
               this.configurationFileHash = configurationHash;
             })
             .catch((error) => {
-              ErrorUtils.handleFileException(
+              handleFileException(
                 this.configurationFile,
                 FileType.ChargingStationConfiguration,
                 error as NodeJS.ErrnoException,
@@ -1603,7 +1829,7 @@ export class ChargingStation {
           );
         }
       } catch (error) {
-        ErrorUtils.handleFileException(
+        handleFileException(
           this.configurationFile,
           FileType.ChargingStationConfiguration,
           error as NodeJS.ErrnoException,
@@ -1654,8 +1880,8 @@ export class ChargingStation {
             skipBufferingOnError: true,
           });
           if (this.isRegistered() === false) {
-            this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
-            await Utils.sleep(
+            this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
+            await sleep(
               this?.bootNotificationResponse?.interval
                 ? this.bootNotificationResponse.interval * 1000
                 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
@@ -1678,7 +1904,7 @@ export class ChargingStation {
       }
       this.wsConnectionRestarted = false;
       this.autoReconnectRetryCount = 0;
-      parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
+      parentPort?.postMessage(buildUpdatedMessage(this));
     } else {
       logger.warn(
         `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
@@ -1692,7 +1918,7 @@ export class ChargingStation {
       case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
       case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
         logger.info(
-          `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
+          `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
             code
           )}' and reason '${reason.toString()}'`
         );
@@ -1701,14 +1927,14 @@ export class ChargingStation {
       // Abnormal close
       default:
         logger.error(
-          `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
+          `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
             code
           )}' and reason '${reason.toString()}'`
         );
         this.started === true && (await this.reconnect());
         break;
     }
-    parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
+    parentPort?.postMessage(buildUpdatedMessage(this));
   }
 
   private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
@@ -1818,7 +2044,7 @@ export class ChargingStation {
             logger.error(`${this.logPrefix()} ${errorMsg}`);
             throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
         }
-        parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
+        parentPort?.postMessage(buildUpdatedMessage(this));
       } else {
         throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
           request,
@@ -1902,7 +2128,10 @@ export class ChargingStation {
 
   private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
     if (this.hasEvses) {
-      for (const evseStatus of this.evses.values()) {
+      for (const [evseId, evseStatus] of this.evses) {
+        if (evseId === 0) {
+          continue;
+        }
         for (const [connectorId, connectorStatus] of evseStatus.connectors) {
           if (connectorStatus.transactionStarted === true) {
             await this.stopTransactionOnConnector(connectorId, reason);
@@ -1974,19 +2203,19 @@ export class ChargingStation {
 
   private getAmperageLimitation(): number | undefined {
     if (
-      Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
+      isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
       ChargingStationConfigurationUtils.getConfigurationKey(
         this,
         this.stationInfo.amperageLimitationOcppKey
       )
     ) {
       return (
-        Utils.convertToInt(
+        convertToInt(
           ChargingStationConfigurationUtils.getConfigurationKey(
             this,
             this.stationInfo.amperageLimitationOcppKey
           )?.value
-        ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
+        ) / getAmperageLimitationUnitDivider(this.stationInfo)
       );
     }
   }
@@ -2009,11 +2238,7 @@ export class ChargingStation {
       for (const [evseId, evseStatus] of this.evses) {
         if (evseId > 0) {
           for (const [connectorId, connectorStatus] of evseStatus.connectors) {
-            const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
-              this,
-              connectorId,
-              connectorStatus
-            );
+            const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus);
             await OCPPServiceUtils.sendAndSetConnectorStatus(
               this,
               connectorId,
@@ -2026,7 +2251,7 @@ export class ChargingStation {
     } else {
       for (const connectorId of this.connectors.keys()) {
         if (connectorId > 0) {
-          const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus(
+          const connectorBootStatus = getBootConnectorStatus(
             this,
             connectorId,
             this.getConnectorStatus(connectorId)
@@ -2112,7 +2337,7 @@ export class ChargingStation {
       this,
       StandardParametersKey.WebSocketPingInterval
     )
-      ? Utils.convertToInt(
+      ? convertToInt(
           ChargingStationConfigurationUtils.getConfigurationKey(
             this,
             StandardParametersKey.WebSocketPingInterval
@@ -2126,13 +2351,13 @@ export class ChargingStation {
         }
       }, webSocketPingInterval * 1000);
       logger.info(
-        `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
+        `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
           webSocketPingInterval
         )}`
       );
     } else if (this.webSocketPingSetInterval) {
       logger.info(
-        `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
+        `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
           webSocketPingInterval
         )}`
       );
@@ -2153,11 +2378,11 @@ export class ChargingStation {
   private getConfiguredSupervisionUrl(): URL {
     let configuredSupervisionUrl: string;
     const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
-    if (Utils.isNotEmptyArray(supervisionUrls)) {
+    if (isNotEmptyArray(supervisionUrls)) {
       let configuredSupervisionUrlIndex: number;
       switch (Configuration.getSupervisionUrlDistribution()) {
         case SupervisionUrlDistribution.RANDOM:
-          configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
+          configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length);
           break;
         case SupervisionUrlDistribution.ROUND_ROBIN:
         case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
@@ -2177,10 +2402,10 @@ export class ChargingStation {
     } else {
       configuredSupervisionUrl = supervisionUrls as string;
     }
-    if (Utils.isNotEmptyString(configuredSupervisionUrl)) {
+    if (isNotEmptyString(configuredSupervisionUrl)) {
       return new URL(configuredSupervisionUrl);
     }
-    const errorMsg = 'No supervision url configured';
+    const errorMsg = 'No supervision url(s) configured';
     logger.error(`${this.logPrefix()} ${errorMsg}`);
     throw new BaseError(`${errorMsg}`);
   }
@@ -2216,9 +2441,9 @@ export class ChargingStation {
       this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
       this.getAutoReconnectMaxRetries() === -1
     ) {
-      this.autoReconnectRetryCount++;
+      ++this.autoReconnectRetryCount;
       const reconnectDelay = this.getReconnectExponentialDelay()
-        ? Utils.exponentialDelay(this.autoReconnectRetryCount)
+        ? exponentialDelay(this.autoReconnectRetryCount)
         : this.getConnectionTimeout() * 1000;
       const reconnectDelayWithdraw = 1000;
       const reconnectTimeout =
@@ -2226,12 +2451,12 @@ export class ChargingStation {
           ? reconnectDelay - reconnectDelayWithdraw
           : 0;
       logger.error(
-        `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
+        `${this.logPrefix()} WebSocket connection retry in ${roundTo(
           reconnectDelay,
           2
         )}ms, timeout ${reconnectTimeout}ms`
       );
-      await Utils.sleep(reconnectDelay);
+      await sleep(reconnectDelay);
       logger.error(
         `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
       );