docs: refine README
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 82ed3b8c0da8ad579e767e6695010352a66b1343..726a2ce9fc63ee06253dc379d003930f9a791888 100644 (file)
@@ -72,6 +72,7 @@ import {
   ReservationTerminationReason,
   type Response,
   StandardParametersKey,
+  type Status,
   type StatusNotificationRequest,
   type StatusNotificationResponse,
   StopTransactionReason,
@@ -91,14 +92,16 @@ import {
   Configuration,
   Constants,
   DCElectricUtils,
-  ErrorUtils,
-  FileUtils,
-  MessageChannelUtils,
   Utils,
   buildChargingStationAutomaticTransactionGeneratorConfiguration,
   buildConnectorsStatus,
   buildEvsesStatus,
+  buildStartedMessage,
+  buildStoppedMessage,
+  buildUpdatedMessage,
+  handleFileException,
   logger,
+  watchJsonFile,
 } from '../utils';
 
 export class ChargingStation {
@@ -135,7 +138,7 @@ export class ChargingStation {
   private readonly sharedLRUCache: SharedLRUCache;
   private webSocketPingSetInterval!: NodeJS.Timeout;
   private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
-  private reservationExpiryDateSetInterval?: NodeJS.Timeout;
+  private reservationExpirationSetInterval?: NodeJS.Timeout;
 
   constructor(index: number, templateFile: string) {
     this.started = false;
@@ -651,11 +654,11 @@ export class ChargingStation {
           this.performanceStatistics?.start();
         }
         if (this.hasFeatureProfile(SupportedFeatureProfiles.Reservation)) {
-          this.startReservationExpiryDateSetInterval();
+          this.startReservationExpirationSetInterval();
         }
         this.openWSConnection();
         // Monitor charging station template file
-        this.templateFileWatcher = FileUtils.watchJsonFile(
+        this.templateFileWatcher = watchJsonFile(
           this.templateFile,
           FileType.ChargingStationTemplate,
           this.logPrefix(),
@@ -692,7 +695,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...`);
@@ -717,7 +720,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...`);
@@ -758,7 +761,7 @@ 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(
@@ -836,7 +839,10 @@ export class ChargingStation {
       | undefined;
     const automaticTransactionGeneratorConfigurationFromFile =
       this.getConfigurationFromFile()?.automaticTransactionGenerator;
-    if (automaticTransactionGeneratorConfigurationFromFile) {
+    if (
+      this.getAutomaticTransactionGeneratorPersistentConfiguration() &&
+      automaticTransactionGeneratorConfigurationFromFile
+    ) {
       automaticTransactionGeneratorConfiguration =
         automaticTransactionGeneratorConfigurationFromFile;
     } else {
@@ -849,6 +855,10 @@ export class ChargingStation {
     };
   }
 
+  public getAutomaticTransactionGeneratorStatuses(): Status[] | undefined {
+    return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses;
+  }
+
   public startAutomaticTransactionGenerator(connectorIds?: number[]): void {
     this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this);
     if (Utils.isNotEmptyArray(connectorIds)) {
@@ -858,8 +868,8 @@ export class ChargingStation {
     } else {
       this.automaticTransactionGenerator?.start();
     }
-    this.saveChargingStationAutomaticTransactionGeneratorConfiguration();
-    parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
+    this.saveAutomaticTransactionGeneratorConfiguration();
+    parentPort?.postMessage(buildUpdatedMessage(this));
   }
 
   public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
@@ -870,8 +880,8 @@ export class ChargingStation {
     } else {
       this.automaticTransactionGenerator?.stop();
     }
-    this.saveChargingStationAutomaticTransactionGeneratorConfiguration();
-    parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
+    this.saveAutomaticTransactionGeneratorConfiguration();
+    parentPort?.postMessage(buildUpdatedMessage(this));
   }
 
   public async stopTransactionOnConnector(
@@ -923,25 +933,15 @@ export class ChargingStation {
   public async addReservation(reservation: Reservation): Promise<void> {
     const [exists, reservationFound] = this.doesReservationExists(reservation);
     if (exists) {
-      await this.removeReservation(reservationFound);
+      await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING);
     }
-    const connectorStatus = this.getConnectorStatus(reservation.connectorId);
-    connectorStatus.reservation = reservation;
-    connectorStatus.status = ConnectorStatusEnum.Reserved;
-    if (reservation.connectorId === 0) {
-      return;
-    }
-    await this.ocppRequestService.requestHandler<
-      StatusNotificationRequest,
-      StatusNotificationResponse
-    >(
+    this.getConnectorStatus(reservation.connectorId).reservation = reservation;
+    await OCPPServiceUtils.sendAndSetConnectorStatus(
       this,
-      RequestCommand.STATUS_NOTIFICATION,
-      OCPPServiceUtils.buildStatusNotificationRequest(
-        this,
-        reservation.connectorId,
-        ConnectorStatusEnum.Reserved
-      )
+      reservation.connectorId,
+      ConnectorStatusEnum.Reserved,
+      null,
+      { send: reservation.connectorId !== 0 }
     );
   }
 
@@ -951,50 +951,41 @@ export class ChargingStation {
   ): Promise<void> {
     const connector = this.getConnectorStatus(reservation.connectorId);
     switch (reason) {
-      case ReservationTerminationReason.TRANSACTION_STARTED: {
+      case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
         delete connector.reservation;
-        if (reservation.connectorId === 0) {
-          connector.status = ConnectorStatusEnum.Available;
-        }
         break;
-      }
-      case ReservationTerminationReason.CONNECTOR_STATE_CHANGED: {
+      case ReservationTerminationReason.TRANSACTION_STARTED:
         delete connector.reservation;
         break;
-      }
-      default: {
-        // ReservationTerminationReason.EXPIRED, ReservationTerminationReason.CANCELED
-        connector.status = ConnectorStatusEnum.Available;
-        delete connector.reservation;
-        await this.ocppRequestService.requestHandler<
-          StatusNotificationRequest,
-          StatusNotificationResponse
-        >(
+      case ReservationTerminationReason.RESERVATION_CANCELED ||
+        ReservationTerminationReason.REPLACE_EXISTING ||
+        ReservationTerminationReason.EXPIRED:
+        await OCPPServiceUtils.sendAndSetConnectorStatus(
           this,
-          RequestCommand.STATUS_NOTIFICATION,
-          OCPPServiceUtils.buildStatusNotificationRequest(
-            this,
-            reservation.connectorId,
-            ConnectorStatusEnum.Available
-          )
+          reservation.connectorId,
+          ConnectorStatusEnum.Available,
+          null,
+          { send: reservation.connectorId !== 0 }
         );
+        delete connector.reservation;
+        break;
+      default:
         break;
-      }
     }
   }
 
-  public getReservationBy(key: string, value: number | string): Reservation {
+  public getReservationBy(filterKey: ReservationFilterKey, value: number | string): Reservation {
     if (this.hasEvses) {
       for (const evse of this.evses.values()) {
         for (const connector of evse.connectors.values()) {
-          if (connector?.reservation?.[key] === value) {
+          if (connector?.reservation?.[filterKey] === value) {
             return connector.reservation;
           }
         }
       }
     } else {
       for (const connector of this.connectors.values()) {
-        if (connector?.reservation?.[key] === value) {
+        if (connector?.reservation?.[filterKey] === value) {
           return connector.reservation;
         }
       }
@@ -1009,27 +1000,33 @@ export class ChargingStation {
     return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation];
   }
 
-  public startReservationExpiryDateSetInterval(customInterval?: number): void {
+  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 CS now`
+        and starts on charging station now`
     );
     // eslint-disable-next-line @typescript-eslint/no-misused-promises
-    this.reservationExpiryDateSetInterval = setInterval(async (): Promise<void> => {
+    this.reservationExpirationSetInterval = setInterval(async (): Promise<void> => {
       if (this.hasEvses) {
         for (const evse of this.evses.values()) {
           for (const connector of evse.connectors.values()) {
             if (connector?.reservation?.expiryDate.toString() < new Date().toISOString()) {
-              await this.removeReservation(connector.reservation);
+              await this.removeReservation(
+                connector.reservation,
+                ReservationTerminationReason.EXPIRED
+              );
             }
           }
         }
       } else {
         for (const connector of this.connectors.values()) {
           if (connector?.reservation?.expiryDate.toString() < new Date().toISOString()) {
-            await this.removeReservation(connector.reservation);
+            await this.removeReservation(
+              connector.reservation,
+              ReservationTerminationReason.EXPIRED
+            );
           }
         }
       }
@@ -1037,8 +1034,8 @@ export class ChargingStation {
   }
 
   public restartReservationExpiryDateSetInterval(): void {
-    this.stopReservationExpiryDateSetInterval();
-    this.startReservationExpiryDateSetInterval();
+    this.stopReservationExpirationSetInterval();
+    this.startReservationExpirationSetInterval();
   }
 
   public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
@@ -1079,8 +1076,8 @@ export class ChargingStation {
 
   private countReservableConnectors(connectors: Map<number, ConnectorStatus>) {
     let reservableConnectors = 0;
-    for (const [id, connector] of connectors) {
-      if (id === 0) {
+    for (const [connectorId, connector] of connectors) {
+      if (connectorId === 0) {
         continue;
       }
       if (connector.status === ConnectorStatusEnum.Available) {
@@ -1131,9 +1128,9 @@ export class ChargingStation {
     return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
   }
 
-  private stopReservationExpiryDateSetInterval(): void {
-    if (this.reservationExpiryDateSetInterval) {
-      clearInterval(this.reservationExpiryDateSetInterval);
+  private stopReservationExpirationSetInterval(): void {
+    if (this.reservationExpirationSetInterval) {
+      clearInterval(this.reservationExpirationSetInterval);
     }
   }
 
@@ -1161,7 +1158,7 @@ export class ChargingStation {
         this.templateFileHash = template.templateHash;
       }
     } catch (error) {
-      ErrorUtils.handleFileException(
+      handleFileException(
         this.templateFile,
         FileType.ChargingStationTemplate,
         error as NodeJS.ErrnoException,
@@ -1280,6 +1277,10 @@ 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}`;
@@ -1295,9 +1296,8 @@ export class ChargingStation {
       `${ChargingStationUtils.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);
@@ -1680,7 +1680,7 @@ export class ChargingStation {
     if (stationTemplate?.Evses) {
       const evsesConfigHash = crypto
         .createHash(Constants.DEFAULT_HASH_ALGORITHM)
-        .update(`${JSON.stringify(stationTemplate?.Evses)}`)
+        .update(JSON.stringify(stationTemplate?.Evses))
         .digest('hex');
       const evsesConfigChanged =
         this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash;
@@ -1741,7 +1741,7 @@ export class ChargingStation {
           this.configurationFileHash = configuration.configurationHash;
         }
       } catch (error) {
-        ErrorUtils.handleFileException(
+        handleFileException(
           this.configurationFile,
           FileType.ChargingStationConfiguration,
           error as NodeJS.ErrnoException,
@@ -1752,8 +1752,10 @@ export class ChargingStation {
     return configuration;
   }
 
-  private saveChargingStationAutomaticTransactionGeneratorConfiguration(): void {
-    this.saveConfiguration();
+  private saveAutomaticTransactionGeneratorConfiguration(): void {
+    if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
+      this.saveConfiguration();
+    }
   }
 
   private saveConnectorsStatus() {
@@ -1774,24 +1776,44 @@ export class ChargingStation {
           Utils.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))
+          .update(
+            JSON.stringify({
+              stationInfo: configurationData.stationInfo,
+              configurationKey: configurationData.configurationKey,
+              automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
+            } as ChargingStationConfiguration)
+          )
           .digest('hex');
         if (this.configurationFileHash !== configurationHash) {
           AsyncLock.acquire(AsyncLockType.configuration)
@@ -1808,7 +1830,7 @@ export class ChargingStation {
               this.configurationFileHash = configurationHash;
             })
             .catch((error) => {
-              ErrorUtils.handleFileException(
+              handleFileException(
                 this.configurationFile,
                 FileType.ChargingStationConfiguration,
                 error as NodeJS.ErrnoException,
@@ -1826,7 +1848,7 @@ export class ChargingStation {
           );
         }
       } catch (error) {
-        ErrorUtils.handleFileException(
+        handleFileException(
           this.configurationFile,
           FileType.ChargingStationConfiguration,
           error as NodeJS.ErrnoException,
@@ -1901,7 +1923,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`
@@ -1931,7 +1953,7 @@ export class ChargingStation {
         this.started === true && (await this.reconnect());
         break;
     }
-    parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
+    parentPort?.postMessage(buildUpdatedMessage(this));
   }
 
   private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
@@ -2041,7 +2063,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,