UI Protocol: add Authorize command support
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index ccd72ff21e96177b2c0fdfa79ed4b9e7623fe0c9..d7b67e6a04b73aa960abc5767dec9338ea0038ee 100644 (file)
@@ -84,14 +84,14 @@ import type OCPPRequestService from './ocpp/OCPPRequestService';
 import SharedLRUCache from './SharedLRUCache';
 
 export default class ChargingStation {
-  public hashId!: string;
   public readonly templateFile: string;
-  public authorizedTagsCache: AuthorizedTagsCache;
   public stationInfo!: ChargingStationInfo;
-  public stopped: boolean;
-  public readonly connectors: Map<number, ConnectorStatus>;
+  public started: boolean;
+  public authorizedTagsCache: AuthorizedTagsCache;
+  public automaticTransactionGenerator!: AutomaticTransactionGenerator;
   public ocppConfiguration!: ChargingStationOcppConfiguration;
   public wsConnection!: WebSocket;
+  public readonly connectors: Map<number, ConnectorStatus>;
   public readonly requests: Map<string, CachedRequest>;
   public performanceStatistics!: PerformanceStatistics;
   public heartbeatSetInterval!: NodeJS.Timeout;
@@ -110,22 +110,21 @@ export default class ChargingStation {
   private autoReconnectRetryCount: number;
   private templateFileWatcher!: fs.FSWatcher;
   private readonly sharedLRUCache: SharedLRUCache;
-  private automaticTransactionGenerator!: AutomaticTransactionGenerator;
   private webSocketPingSetInterval!: NodeJS.Timeout;
   private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
 
   constructor(index: number, templateFile: string) {
     this.index = index;
     this.templateFile = templateFile;
-    this.stopped = false;
-    this.wsConnectionRestarted = false;
-    this.autoReconnectRetryCount = 0;
-    this.sharedLRUCache = SharedLRUCache.getInstance();
-    this.authorizedTagsCache = AuthorizedTagsCache.getInstance();
     this.connectors = new Map<number, ConnectorStatus>();
     this.requests = new Map<string, CachedRequest>();
     this.messageBuffer = new Set<string>();
+    this.sharedLRUCache = SharedLRUCache.getInstance();
+    this.authorizedTagsCache = AuthorizedTagsCache.getInstance();
     this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this);
+    this.started = false;
+    this.wsConnectionRestarted = false;
+    this.autoReconnectRetryCount = 0;
 
     this.initialize();
   }
@@ -234,7 +233,7 @@ export default class ChargingStation {
     return this.connectors.get(0) ? this.connectors.size - 1 : this.connectors.size;
   }
 
-  public getConnectorStatus(id: number): ConnectorStatus {
+  public getConnectorStatus(id: number): ConnectorStatus | undefined {
     return this.connectors.get(id);
   }
 
@@ -332,22 +331,18 @@ export default class ChargingStation {
     }
   }
 
-  public getEnergyActiveImportRegisterByTransactionId(transactionId: number): number {
-    const transactionConnectorStatus = this.getConnectorStatus(
-      this.getConnectorIdByTransactionId(transactionId)
+  public getEnergyActiveImportRegisterByTransactionId(
+    transactionId: number,
+    meterStop = false
+  ): number {
+    return this.getEnergyActiveImportRegister(
+      this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)),
+      meterStop
     );
-    if (this.getMeteringPerTransaction()) {
-      return transactionConnectorStatus?.transactionEnergyActiveImportRegisterValue ?? 0;
-    }
-    return transactionConnectorStatus?.energyActiveImportRegisterValue ?? 0;
   }
 
   public getEnergyActiveImportRegisterByConnectorId(connectorId: number): number {
-    const connectorStatus = this.getConnectorStatus(connectorId);
-    if (this.getMeteringPerTransaction()) {
-      return connectorStatus?.transactionEnergyActiveImportRegisterValue ?? 0;
-    }
-    return connectorStatus?.energyActiveImportRegisterValue ?? 0;
+    return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId));
   }
 
   public getAuthorizeRemoteTxRequests(): boolean {
@@ -430,13 +425,13 @@ export default class ChargingStation {
       );
       return;
     }
-    if (!this.getConnectorStatus(connectorId)?.transactionStarted) {
+    if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
       logger.error(
         `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`
       );
       return;
     } else if (
-      this.getConnectorStatus(connectorId)?.transactionStarted &&
+      this.getConnectorStatus(connectorId)?.transactionStarted === true &&
       !this.getConnectorStatus(connectorId)?.transactionId
     ) {
       logger.error(
@@ -503,7 +498,7 @@ export default class ChargingStation {
             this.initialize();
             // Restart the ATG
             this.stopAutomaticTransactionGenerator();
-            if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable) {
+            if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
               this.startAutomaticTransactionGenerator();
             }
             if (this.getEnableStatistics()) {
@@ -524,8 +519,7 @@ export default class ChargingStation {
     parentPort.postMessage(MessageChannelUtils.buildStartedMessage(this));
   }
 
-  public async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
-    // Stop message sequence
+  public async stop(reason?: StopTransactionReason): Promise<void> {
     await this.stopMessageSequence(reason);
     for (const connectorId of this.connectors.keys()) {
       if (connectorId > 0) {
@@ -548,7 +542,7 @@ export default class ChargingStation {
     this.templateFileWatcher.close();
     this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
     this.bootNotificationResponse = null;
-    this.stopped = true;
+    this.started = false;
     parentPort.postMessage(MessageChannelUtils.buildStoppedMessage(this));
   }
 
@@ -565,58 +559,6 @@ export default class ChargingStation {
     }
   }
 
-  public getChargingProfilePowerLimit(connectorId: number): number | undefined {
-    let limit: number, matchingChargingProfile: ChargingProfile;
-    let chargingProfiles: ChargingProfile[] = [];
-    // Get charging profiles for connector and sort by stack level
-    chargingProfiles = this.getConnectorStatus(connectorId).chargingProfiles.sort(
-      (a, b) => b.stackLevel - a.stackLevel
-    );
-    // Get profiles on connector 0
-    if (this.getConnectorStatus(0).chargingProfiles) {
-      chargingProfiles.push(
-        ...this.getConnectorStatus(0).chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
-      );
-    }
-    if (!Utils.isEmptyArray(chargingProfiles)) {
-      const result = ChargingStationUtils.getLimitFromChargingProfiles(
-        chargingProfiles,
-        this.logPrefix()
-      );
-      if (!Utils.isNullOrUndefined(result)) {
-        limit = result.limit;
-        matchingChargingProfile = result.matchingChargingProfile;
-        switch (this.getCurrentOutType()) {
-          case CurrentType.AC:
-            limit =
-              matchingChargingProfile.chargingSchedule.chargingRateUnit ===
-              ChargingRateUnitType.WATT
-                ? limit
-                : ACElectricUtils.powerTotal(this.getNumberOfPhases(), this.getVoltageOut(), limit);
-            break;
-          case CurrentType.DC:
-            limit =
-              matchingChargingProfile.chargingSchedule.chargingRateUnit ===
-              ChargingRateUnitType.WATT
-                ? limit
-                : DCElectricUtils.power(this.getVoltageOut(), limit);
-        }
-
-        const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
-        if (limit > connectorMaximumPower) {
-          logger.error(
-            `${this.logPrefix()} Charging profile id ${
-              matchingChargingProfile.chargingProfileId
-            } limit is greater than connector id ${connectorId} maximum, dump charging profiles' stack: %j`,
-            this.getConnectorStatus(connectorId).chargingProfiles
-          );
-          limit = connectorMaximumPower;
-        }
-      }
-    }
-    return limit;
-  }
-
   public setChargingProfile(connectorId: number, cp: ChargingProfile): void {
     if (Utils.isNullOrUndefined(this.getConnectorStatus(connectorId).chargingProfiles)) {
       logger.error(
@@ -660,6 +602,7 @@ export default class ChargingStation {
     this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
     delete this.getConnectorStatus(connectorId).transactionBeginMeterValue;
     this.stopMeterValues(connectorId);
+    parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
   }
 
   public hasFeatureProfile(featureProfile: SupportedFeatureProfiles) {
@@ -748,26 +691,71 @@ export default class ChargingStation {
     }
   }
 
-  public startAutomaticTransactionGenerator() {
+  public startAutomaticTransactionGenerator(connectorIds?: number[]): void {
     if (!this.automaticTransactionGenerator) {
       this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(
         this.getAutomaticTransactionGeneratorConfigurationFromTemplate(),
         this
       );
     }
-    if (!this.automaticTransactionGenerator.started) {
+    if (!Utils.isEmptyArray(connectorIds)) {
+      for (const connectorId of connectorIds) {
+        this.automaticTransactionGenerator.startConnector(connectorId);
+      }
+    } else {
       this.automaticTransactionGenerator.start();
     }
   }
 
-  public stopAutomaticTransactionGenerator(): void {
-    if (this.automaticTransactionGenerator?.started) {
-      this.automaticTransactionGenerator.stop();
+  public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
+    if (!Utils.isEmptyArray(connectorIds)) {
+      for (const connectorId of connectorIds) {
+        this.automaticTransactionGenerator?.stopConnector(connectorId);
+      }
+    } else {
+      this.automaticTransactionGenerator?.stop();
       this.automaticTransactionGenerator = null;
     }
   }
 
-  private flushMessageBuffer() {
+  public async stopTransactionOnConnector(
+    connectorId: number,
+    reason = StopTransactionReason.NONE
+  ): Promise<StopTransactionResponse> {
+    const transactionId = this.getConnectorStatus(connectorId).transactionId;
+    if (
+      this.getBeginEndMeterValues() &&
+      this.getOcppStrictCompliance() &&
+      !this.getOutOfOrderEndMeterValues()
+    ) {
+      // FIXME: Implement OCPP version agnostic helpers
+      const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
+        this,
+        connectorId,
+        this.getEnergyActiveImportRegisterByTransactionId(transactionId)
+      );
+      await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
+        this,
+        RequestCommand.METER_VALUES,
+        {
+          connectorId,
+          transactionId,
+          meterValue: [transactionEndMeterValue],
+        }
+      );
+    }
+    return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
+      this,
+      RequestCommand.STOP_TRANSACTION,
+      {
+        transactionId,
+        meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
+        reason,
+      }
+    );
+  }
+
+  private flushMessageBuffer(): void {
     if (this.messageBuffer.size > 0) {
       this.messageBuffer.forEach((message) => {
         // TODO: evaluate the need to track performance
@@ -841,6 +829,7 @@ export default class ChargingStation {
     );
     const stationInfo: ChargingStationInfo =
       ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
+    stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
     stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
       this.index,
       stationTemplate
@@ -950,20 +939,19 @@ export default class ChargingStation {
   }
 
   private initialize(): void {
-    this.hashId = ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile());
-    logger.info(`${this.logPrefix()} Charging station hashId '${this.hashId}'`);
     this.configurationFile = path.join(
       path.dirname(this.templateFile.replace('station-templates', 'configurations')),
-      this.hashId + '.json'
+      ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile()) + '.json'
     );
     this.stationInfo = this.getStationInfo();
     this.saveStationInfo();
+    logger.info(`${this.logPrefix()} Charging station hashId '${this.stationInfo.hashId}'`);
     // Avoid duplication of connectors related information in RAM
     this.stationInfo?.Connectors && delete this.stationInfo.Connectors;
     this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
     if (this.getEnableStatistics()) {
       this.performanceStatistics = PerformanceStatistics.getInstance(
-        this.hashId,
+        this.stationInfo.hashId,
         this.stationInfo.chargingStationId,
         this.configuredSupervisionUrl
       );
@@ -1230,7 +1218,11 @@ export default class ChargingStation {
     }
     // Initialize transaction attributes on connectors
     for (const connectorId of this.connectors.keys()) {
-      if (connectorId > 0 && !this.getConnectorStatus(connectorId)?.transactionStarted) {
+      if (
+        connectorId > 0 &&
+        (this.getConnectorStatus(connectorId).transactionStarted === undefined ||
+          this.getConnectorStatus(connectorId).transactionStarted === false)
+      ) {
         this.initializeConnectorStatus(connectorId);
       }
     }
@@ -1389,9 +1381,10 @@ export default class ChargingStation {
           `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
         );
       }
-      this.stopped && (this.stopped = false);
+      this.started === false && (this.started = true);
       this.autoReconnectRetryCount = 0;
       this.wsConnectionRestarted = false;
+      parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
     } else {
       logger.warn(
         `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
@@ -1405,7 +1398,7 @@ export default class ChargingStation {
       case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
       case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
         logger.info(
-          `${this.logPrefix()} WebSocket normally closed with status '${ChargingStationUtils.getWebSocketCloseEventStatusString(
+          `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
             code
           )}' and reason '${reason}'`
         );
@@ -1414,13 +1407,14 @@ export default class ChargingStation {
       // Abnormal close
       default:
         logger.error(
-          `${this.logPrefix()} WebSocket abnormally closed with status '${ChargingStationUtils.getWebSocketCloseEventStatusString(
+          `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
             code
           )}' and reason '${reason}'`
         );
         await this.reconnect(code);
         break;
     }
+    parentPort.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
   }
 
   private async onMessage(data: Data): Promise<void> {
@@ -1578,6 +1572,24 @@ export default class ChargingStation {
     logger.error(this.logPrefix() + ' WebSocket error:', error);
   }
 
+  private getEnergyActiveImportRegister(
+    connectorStatus: ConnectorStatus,
+    meterStop = false
+  ): number {
+    if (this.getMeteringPerTransaction()) {
+      return (
+        (meterStop === true
+          ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
+          : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
+      );
+    }
+    return (
+      (meterStop === true
+        ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
+        : connectorStatus?.energyActiveImportRegisterValue) ?? 0
+    );
+  }
+
   private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean | undefined {
     const localStationInfo = stationInfo ?? this.stationInfo;
     return !Utils.isUndefined(localStationInfo.useConnectorId0)
@@ -1588,13 +1600,21 @@ export default class ChargingStation {
   private getNumberOfRunningTransactions(): number {
     let trxCount = 0;
     for (const connectorId of this.connectors.keys()) {
-      if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
+      if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
         trxCount++;
       }
     }
     return trxCount;
   }
 
+  private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
+    for (const connectorId of this.connectors.keys()) {
+      if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
+        await this.stopTransactionOnConnector(connectorId, reason);
+      }
+    }
+  }
+
   // 0 for disabling
   private getConnectionTimeout(): number | undefined {
     if (
@@ -1680,6 +1700,57 @@ export default class ChargingStation {
     }
   }
 
+  private getChargingProfilePowerLimit(connectorId: number): number | undefined {
+    let limit: number, matchingChargingProfile: ChargingProfile;
+    let chargingProfiles: ChargingProfile[] = [];
+    // Get charging profiles for connector and sort by stack level
+    chargingProfiles = this.getConnectorStatus(connectorId).chargingProfiles.sort(
+      (a, b) => b.stackLevel - a.stackLevel
+    );
+    // Get profiles on connector 0
+    if (this.getConnectorStatus(0).chargingProfiles) {
+      chargingProfiles.push(
+        ...this.getConnectorStatus(0).chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
+      );
+    }
+    if (!Utils.isEmptyArray(chargingProfiles)) {
+      const result = ChargingStationUtils.getLimitFromChargingProfiles(
+        chargingProfiles,
+        this.logPrefix()
+      );
+      if (!Utils.isNullOrUndefined(result)) {
+        limit = result.limit;
+        matchingChargingProfile = result.matchingChargingProfile;
+        switch (this.getCurrentOutType()) {
+          case CurrentType.AC:
+            limit =
+              matchingChargingProfile.chargingSchedule.chargingRateUnit ===
+              ChargingRateUnitType.WATT
+                ? limit
+                : ACElectricUtils.powerTotal(this.getNumberOfPhases(), this.getVoltageOut(), limit);
+            break;
+          case CurrentType.DC:
+            limit =
+              matchingChargingProfile.chargingSchedule.chargingRateUnit ===
+              ChargingRateUnitType.WATT
+                ? limit
+                : DCElectricUtils.power(this.getVoltageOut(), limit);
+        }
+        const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
+        if (limit > connectorMaximumPower) {
+          logger.error(
+            `${this.logPrefix()} Charging profile id ${
+              matchingChargingProfile.chargingProfileId
+            } limit is greater than connector id ${connectorId} maximum, dump charging profiles' stack: %j`,
+            this.getConnectorStatus(connectorId).chargingProfiles
+          );
+          limit = connectorMaximumPower;
+        }
+      }
+    }
+    return limit;
+  }
+
   private async startMessageSequence(): Promise<void> {
     if (this.stationInfo?.autoRegister) {
       await this.ocppRequestService.requestHandler<
@@ -1711,7 +1782,7 @@ export default class ChargingStation {
       if (connectorId === 0) {
         continue;
       } else if (
-        !this.stopped &&
+        this.started === true &&
         !this.getConnectorStatus(connectorId)?.status &&
         this.getConnectorStatus(connectorId)?.bootStatus
       ) {
@@ -1727,7 +1798,7 @@ export default class ChargingStation {
         this.getConnectorStatus(connectorId).status =
           this.getConnectorStatus(connectorId).bootStatus;
       } else if (
-        this.stopped &&
+        this.started === false &&
         this.getConnectorStatus(connectorId)?.status &&
         this.getConnectorStatus(connectorId)?.bootStatus
       ) {
@@ -1742,7 +1813,7 @@ export default class ChargingStation {
         });
         this.getConnectorStatus(connectorId).status =
           this.getConnectorStatus(connectorId).bootStatus;
-      } else if (!this.stopped && this.getConnectorStatus(connectorId)?.status) {
+      } else if (this.started === true && this.getConnectorStatus(connectorId)?.status) {
         // Send previous status at template reload
         await this.ocppRequestService.requestHandler<
           StatusNotificationRequest,
@@ -1766,7 +1837,7 @@ export default class ChargingStation {
       }
     }
     // Start the ATG
-    if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable) {
+    if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
       this.startAutomaticTransactionGenerator();
     }
   }
@@ -1779,46 +1850,10 @@ export default class ChargingStation {
     // Stop heartbeat
     this.stopHeartbeat();
     // Stop ongoing transactions
-    if (this.getNumberOfRunningTransactions() > 0) {
-      if (this.automaticTransactionGenerator?.started) {
-        this.stopAutomaticTransactionGenerator();
-      } else {
-        for (const connectorId of this.connectors.keys()) {
-          if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
-            const transactionId = this.getConnectorStatus(connectorId).transactionId;
-            if (
-              this.getBeginEndMeterValues() &&
-              this.getOcppStrictCompliance() &&
-              !this.getOutOfOrderEndMeterValues()
-            ) {
-              // FIXME: Implement OCPP version agnostic helpers
-              const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
-                this,
-                connectorId,
-                this.getEnergyActiveImportRegisterByTransactionId(transactionId)
-              );
-              await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
-                this,
-                RequestCommand.METER_VALUES,
-                {
-                  connectorId,
-                  transactionId,
-                  meterValue: [transactionEndMeterValue],
-                }
-              );
-            }
-            await this.ocppRequestService.requestHandler<
-              StopTransactionRequest,
-              StopTransactionResponse
-            >(this, RequestCommand.STOP_TRANSACTION, {
-              transactionId,
-              meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId),
-              idTag: this.getTransactionIdTag(transactionId),
-              reason,
-            });
-          }
-        }
-      }
+    if (this.automaticTransactionGenerator?.started === true) {
+      this.stopAutomaticTransactionGenerator();
+    } else {
+      await this.stopRunningTransactions(reason);
     }
   }
 
@@ -1963,7 +1998,7 @@ export default class ChargingStation {
     // Stop heartbeat
     this.stopHeartbeat();
     // Stop the ATG if needed
-    if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure) {
+    if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
       this.stopAutomaticTransactionGenerator();
     }
     if (