feat: make evse and connector configurations in template mutually
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 4f65a4e6a0c28122cf37e877daf7b4d953973670..96bf6c1b3370fc29a772fc5baa01d5ef172c5509 100644 (file)
@@ -53,6 +53,7 @@ import {
   type ErrorCallback,
   type ErrorResponse,
   ErrorType,
+  type EvseStatus,
   FileType,
   FirmwareStatus,
   type FirmwareStatusNotificationRequest,
@@ -108,6 +109,7 @@ export class ChargingStation {
   public ocppConfiguration!: ChargingStationOcppConfiguration | undefined;
   public wsConnection!: WebSocket | null;
   public readonly connectors: Map<number, ConnectorStatus>;
+  public readonly evses: Map<number, EvseStatus>;
   public readonly requests: Map<string, CachedRequest>;
   public performanceStatistics!: PerformanceStatistics | undefined;
   public heartbeatSetInterval!: NodeJS.Timeout;
@@ -119,6 +121,7 @@ export class ChargingStation {
   private configurationFile!: string;
   private configurationFileHash!: string;
   private connectorsConfigurationHash!: string;
+  private evsesConfigurationHash!: string;
   private ocppIncomingRequestService!: OCPPIncomingRequestService;
   private readonly messageBuffer: Set<string>;
   private configuredSupervisionUrl!: URL;
@@ -138,6 +141,7 @@ export class ChargingStation {
     this.index = index;
     this.templateFile = templateFile;
     this.connectors = new Map<number, ConnectorStatus>();
+    this.evses = new Map<number, EvseStatus>();
     this.requests = new Map<string, CachedRequest>();
     this.messageBuffer = new Set<string>();
     this.sharedLRUCache = SharedLRUCache.getInstance();
@@ -233,11 +237,11 @@ export class ChargingStation {
   }
 
   public isChargingStationAvailable(): boolean {
-    return this.getConnectorStatus(0)?.availability === AvailabilityType.OPERATIVE;
+    return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative;
   }
 
   public isConnectorAvailable(id: number): boolean {
-    return id > 0 && this.getConnectorStatus(id)?.availability === AvailabilityType.OPERATIVE;
+    return id > 0 && this.getConnectorStatus(id)?.availability === AvailabilityType.Operative;
   }
 
   public getNumberOfConnectors(): number {
@@ -471,19 +475,19 @@ export class ChargingStation {
   public startMeterValues(connectorId: number, interval: number): void {
     if (connectorId === 0) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId.toString()}`
+        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}`
       );
       return;
     }
     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 (
@@ -491,7 +495,7 @@ export class ChargingStation {
       Utils.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;
     }
@@ -796,7 +800,7 @@ export class ChargingStation {
 
   private flushMessageBuffer(): void {
     if (this.messageBuffer.size > 0) {
-      this.messageBuffer.forEach((message) => {
+      for (const message of this.messageBuffer.values()) {
         let beginId: string;
         let commandName: RequestCommand;
         const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
@@ -813,7 +817,7 @@ export class ChargingStation {
           )} payload sent: ${message}`
         );
         this.messageBuffer.delete(message);
-      });
+      }
     }
   }
 
@@ -925,27 +929,40 @@ export class ChargingStation {
       this.templateFile,
       this.logPrefix()
     );
-    const templateMaxConnectors =
-      ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
-    ChargingStationUtils.checkTemplateMaxConnectors(
-      templateMaxConnectors,
-      this.templateFile,
-      this.logPrefix()
-    );
-    if (
-      configuredMaxConnectors >
-        (stationTemplate?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
-      !stationTemplate?.randomConnectors
-    ) {
-      logger.warn(
-        `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
-          this.templateFile
-        }, forcing random connector configurations affectation`
+    // Build evses or connectors if needed (FIXME: should be factored out)
+    if (stationInfo?.Connectors && !stationInfo?.Evses) {
+      const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
+        stationTemplate.Connectors
       );
-      stationInfo.randomConnectors = true;
+      ChargingStationUtils.checkTemplateMaxConnectors(
+        templateMaxConnectors,
+        this.templateFile,
+        this.logPrefix()
+      );
+      if (
+        configuredMaxConnectors >
+          (stationTemplate?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
+        !stationTemplate?.randomConnectors
+      ) {
+        logger.warn(
+          `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
+            this.templateFile
+          }, forcing random connector configurations affectation`
+        );
+        stationInfo.randomConnectors = true;
+      }
+      this.initializeConnectors(stationInfo, configuredMaxConnectors);
+    } else if (stationInfo?.Evses && !stationInfo?.Connectors) {
+      this.initializeEvses(stationInfo);
+    } else if (stationInfo?.Evses && stationInfo?.Connectors) {
+      const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`;
+      logger.error(`${this.logPrefix()} ${errorMsg}`);
+      throw new BaseError(errorMsg);
+    } else {
+      const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`;
+      logger.error(`${this.logPrefix()} ${errorMsg}`);
+      throw new BaseError(errorMsg);
     }
-    // Build connectors if needed (FIXME: should be factored out)
-    this.initializeConnectors(stationInfo, configuredMaxConnectors, templateMaxConnectors);
     stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
     ChargingStationUtils.createStationInfoHash(stationInfo);
     return stationInfo;
@@ -962,7 +979,10 @@ export class ChargingStation {
   private getStationInfo(): ChargingStationInfo {
     const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
     const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
-    // Priority: charging station info from template > charging station info from configuration file > charging station info attribute
+    // Priority:
+    // 1. charging station info from template
+    // 2. charging station info from configuration file
+    // 3. charging station info attribute
     if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
       if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) {
         return this.stationInfo;
@@ -1240,8 +1260,7 @@ export class ChargingStation {
 
   private initializeConnectors(
     stationInfo: ChargingStationInfo,
-    configuredMaxConnectors: number,
-    templateMaxConnectors: number
+    configuredMaxConnectors: number
   ): void {
     if (!stationInfo?.Connectors && this.connectors.size === 0) {
       const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
@@ -1252,7 +1271,7 @@ export class ChargingStation {
       logger.warn(
         `${this.logPrefix()} Charging station information from template ${
           this.templateFile
-        } with no connector Id 0 configuration`
+        } with no connector id 0 configuration`
       );
     }
     if (stationInfo?.Connectors) {
@@ -1265,7 +1284,7 @@ export class ChargingStation {
       if (this.connectors?.size === 0 || connectorsConfigChanged) {
         connectorsConfigChanged && this.connectors.clear();
         this.connectorsConfigurationHash = connectorsConfigHash;
-        // Add connector Id 0
+        // Add connector id 0
         let lastConnector = '0';
         for (lastConnector in stationInfo?.Connectors) {
           const connectorStatus = stationInfo?.Connectors[lastConnector];
@@ -1280,13 +1299,16 @@ export class ChargingStation {
               lastConnectorId,
               Utils.cloneObject<ConnectorStatus>(connectorStatus)
             );
-            this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
+            this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.Operative;
             if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
               this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
             }
           }
         }
         // Generate all connectors
+        const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
+          stationInfo?.Connectors
+        );
         if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
           for (let index = 1; index <= configuredMaxConnectors; index++) {
             const randConnectorId = stationInfo?.randomConnectors
@@ -1295,7 +1317,7 @@ export class ChargingStation {
             const connectorStatus = stationInfo?.Connectors[randConnectorId.toString()];
             this.checkStationInfoConnectorStatus(randConnectorId, connectorStatus);
             this.connectors.set(index, Utils.cloneObject<ConnectorStatus>(connectorStatus));
-            this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
+            this.getConnectorStatus(index).availability = AvailabilityType.Operative;
             if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
               this.getConnectorStatus(index).chargingProfiles = [];
             }
@@ -1309,7 +1331,7 @@ export class ChargingStation {
         } with no connectors configuration defined, using already defined connectors`
       );
     }
-    // Initialize transaction attributes on connectors
+    // Initialize connectors status
     for (const connectorId of this.connectors.keys()) {
       if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
         logger.warn(
@@ -1322,8 +1344,83 @@ export class ChargingStation {
         connectorId > 0 &&
         Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionStarted)
       ) {
-        this.initializeConnectorStatus(connectorId);
+        this.initializeConnectorStatus(this.getConnectorStatus(connectorId));
+      }
+    }
+  }
+
+  private buildConnectorsMap(
+    connectors: Record<string, ConnectorStatus>
+  ): Map<number, ConnectorStatus> {
+    const connectorsMap = new Map<number, ConnectorStatus>();
+    for (const connector in connectors) {
+      const connectorStatus = connectors[connector];
+      const connectorId = Utils.convertToInt(connector);
+      this.checkStationInfoConnectorStatus(connectorId, connectorStatus);
+      connectorsMap.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
+      connectorsMap.get(connectorId).availability = AvailabilityType.Operative;
+      if (Utils.isUndefined(connectorsMap.get(connectorId)?.chargingProfiles)) {
+        connectorsMap.get(connectorId).chargingProfiles = [];
+      }
+    }
+    return connectorsMap;
+  }
+
+  private initializeConnectorsMapStatus(connectors: Map<number, ConnectorStatus>): void {
+    for (const connectorId of connectors.keys()) {
+      if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
+        logger.warn(
+          `${this.logPrefix()} Connector ${connectorId} at initialization has a transaction started: ${
+            connectors.get(connectorId)?.transactionId
+          }`
+        );
+      }
+      if (
+        connectorId > 0 &&
+        Utils.isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
+      ) {
+        this.initializeConnectorStatus(connectors.get(connectorId));
+      }
+    }
+  }
+
+  private initializeEvses(stationInfo: ChargingStationInfo): void {
+    if (!stationInfo?.Evses && this.evses.size === 0) {
+      const logMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
+      logger.warn(`${this.logPrefix()} ${logMsg}`);
+      return;
+    }
+    if (!stationInfo?.Evses[0]) {
+      logger.warn(
+        `${this.logPrefix()} Charging station information from template ${
+          this.templateFile
+        } with no evse id 0 configuration`
+      );
+    }
+    if (stationInfo?.Evses) {
+      const evsesConfigHash = crypto
+        .createHash(Constants.DEFAULT_HASH_ALGORITHM)
+        .update(`${JSON.stringify(stationInfo?.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;
+        for (const evse in stationInfo?.Evses) {
+          this.evses.set(Utils.convertToInt(evse), {
+            connectors: this.buildConnectorsMap(stationInfo?.Evses[evse]?.Connectors),
+            availability: AvailabilityType.Operative,
+          });
+          this.initializeConnectorsMapStatus(this.evses.get(Utils.convertToInt(evse))?.connectors);
+        }
       }
+    } else {
+      logger.warn(
+        `${this.logPrefix()} Charging station information from template ${
+          this.templateFile
+        } with no evses configuration defined, using already defined evses`
+      );
     }
   }
 
@@ -1844,15 +1941,7 @@ export class ChargingStation {
         // Set default status
         connectorStatus = ConnectorStatusEnum.Available;
       }
-      await this.ocppRequestService.requestHandler<
-        StatusNotificationRequest,
-        StatusNotificationResponse
-      >(
-        this,
-        RequestCommand.STATUS_NOTIFICATION,
-        OCPPServiceUtils.buildStatusNotificationRequest(this, connectorId, connectorStatus)
-      );
-      this.getConnectorStatus(connectorId).status = connectorStatus;
+      await OCPPServiceUtils.sendAndSetConnectorStatus(this, connectorId, connectorStatus);
     }
     if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
       await this.ocppRequestService.requestHandler<
@@ -2052,12 +2141,12 @@ export class ChargingStation {
     return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
   }
 
-  private initializeConnectorStatus(connectorId: number): void {
-    this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
-    this.getConnectorStatus(connectorId).idTagAuthorized = false;
-    this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
-    this.getConnectorStatus(connectorId).transactionStarted = false;
-    this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
-    this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
+  private initializeConnectorStatus(connectorStatus: ConnectorStatus): void {
+    connectorStatus.idTagLocalAuthorized = false;
+    connectorStatus.idTagAuthorized = false;
+    connectorStatus.transactionRemoteStarted = false;
+    connectorStatus.transactionStarted = false;
+    connectorStatus.energyActiveImportRegisterValue = 0;
+    connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
   }
 }