Optimize a loop in the worker related code.
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 0f2034fa30f5c2ff5d774a8f289a80edbdccfbc8..5c83b7f2b9a044d456b8199156e7d09d40c26386 100644 (file)
@@ -1,30 +1,31 @@
 import { BootNotificationResponse, RegistrationStatus } from '../types/ocpp/Responses';
 import ChargingStationConfiguration, { ConfigurationKey } from '../types/ChargingStationConfiguration';
-import ChargingStationTemplate, { CurrentOutType, PowerUnits, VoltageOut } from '../types/ChargingStationTemplate';
+import ChargingStationTemplate, { CurrentType, PowerUnits, Voltage } from '../types/ChargingStationTemplate';
 import { ConnectorPhaseRotation, StandardParametersKey, SupportedFeatureProfiles } from '../types/ocpp/Configuration';
 import Connectors, { Connector, SampledValueTemplate } from '../types/Connectors';
 import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues';
-import { PerformanceObserver, performance } from 'perf_hooks';
 import Requests, { AvailabilityType, BootNotificationRequest, IncomingRequest, IncomingRequestCommand } from '../types/ocpp/Requests';
-import WebSocket, { MessageEvent } from 'ws';
+import WebSocket, { ClientOptions, MessageEvent } from 'ws';
 
 import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
 import { ChargePointStatus } from '../types/ocpp/ChargePointStatus';
 import { ChargingProfile } from '../types/ocpp/ChargingProfile';
 import ChargingStationInfo from '../types/ChargingStationInfo';
+import { ClientRequestArgs } from 'http';
 import Configuration from '../utils/Configuration';
 import Constants from '../utils/Constants';
 import FileUtils from '../utils/FileUtils';
 import { MessageType } from '../types/ocpp/MessageType';
-import OCPP16IncomingRequestService from './ocpp/1.6/OCCP16IncomingRequestService';
+import OCPP16IncomingRequestService from './ocpp/1.6/OCPP16IncomingRequestService';
 import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService';
 import OCPP16ResponseService from './ocpp/1.6/OCPP16ResponseService';
-import OCPPError from './OcppError';
+import OCPPError from './OCPPError';
 import OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService';
 import OCPPRequestService from './ocpp/OCPPRequestService';
 import { OCPPVersion } from '../types/ocpp/OCPPVersion';
 import PerformanceStatistics from '../utils/PerformanceStatistics';
 import { StopTransactionReason } from '../types/ocpp/Transaction';
+import { URL } from 'url';
 import Utils from '../utils/Utils';
 import { WebSocketCloseEventStatusCode } from '../types/WebSocket';
 import crypto from 'crypto';
@@ -50,12 +51,10 @@ export default class ChargingStation {
   private bootNotificationRequest!: BootNotificationRequest;
   private bootNotificationResponse!: BootNotificationResponse | null;
   private connectorsConfigurationHash!: string;
-  private supervisionUrl!: string;
-  private wsConnectionUrl!: string;
+  private wsConnectionUrl!: URL;
   private hasSocketRestarted: boolean;
   private autoReconnectRetryCount: number;
   private automaticTransactionGeneration!: AutomaticTransactionGenerator;
-  private performanceObserver!: PerformanceObserver;
   private webSocketPingSetInterval!: NodeJS.Timeout;
 
   constructor(index: number, stationTemplateFile: string) {
@@ -91,11 +90,15 @@ export default class ChargingStation {
     return !Utils.isUndefined(this.stationInfo.enableStatistics) ? this.stationInfo.enableStatistics : true;
   }
 
+  public getMayAuthorizeAtRemoteStart(): boolean | undefined {
+    return this.stationInfo.mayAuthorizeAtRemoteStart ?? true;
+  }
+
   public getNumberOfPhases(): number | undefined {
     switch (this.getCurrentOutType()) {
-      case CurrentOutType.AC:
+      case CurrentType.AC:
         return !Utils.isUndefined(this.stationInfo.numberOfPhases) ? this.stationInfo.numberOfPhases : 3;
-      case CurrentOutType.DC:
+      case CurrentType.DC:
         return 0;
     }
   }
@@ -120,23 +123,23 @@ export default class ChargingStation {
     return this.connectors[id];
   }
 
-  public getCurrentOutType(): CurrentOutType | undefined {
-    return this.stationInfo.currentOutType ?? CurrentOutType.AC;
+  public getCurrentOutType(): CurrentType | undefined {
+    return this.stationInfo.currentOutType ?? CurrentType.AC;
   }
 
   public getVoltageOut(): number | undefined {
     const errMsg = `${this.logPrefix()} Unknown ${this.getCurrentOutType()} currentOutType in template file ${this.stationTemplateFile}, cannot define default voltage out`;
     let defaultVoltageOut: number;
     switch (this.getCurrentOutType()) {
-      case CurrentOutType.AC:
-        defaultVoltageOut = VoltageOut.VOLTAGE_230;
+      case CurrentType.AC:
+        defaultVoltageOut = Voltage.VOLTAGE_230;
         break;
-      case CurrentOutType.DC:
-        defaultVoltageOut = VoltageOut.VOLTAGE_400;
+      case CurrentType.DC:
+        defaultVoltageOut = Voltage.VOLTAGE_400;
         break;
       default:
         logger.error(errMsg);
-        throw Error(errMsg);
+        throw new Error(errMsg);
     }
     return !Utils.isUndefined(this.stationInfo.voltageOut) ? this.stationInfo.voltageOut : defaultVoltageOut;
   }
@@ -144,7 +147,7 @@ export default class ChargingStation {
   public getTransactionIdTag(transactionId: number): string | undefined {
     for (const connector in this.connectors) {
       if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
-        return this.getConnector(Utils.convertToInt(connector)).idTag;
+        return this.getConnector(Utils.convertToInt(connector)).transactionIdTag;
       }
     }
   }
@@ -219,12 +222,15 @@ export default class ChargingStation {
       return;
     }
     if (measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER && !this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(measurand)) {
-      logger.warn(`${this.logPrefix()} Trying to get MeterValues measurand ${measurand} ${phase ? `on phase ${phase} ` : ''}in template on connectorId ${connectorId} not found in '${StandardParametersKey.MeterValuesSampledData}' OCPP parameter`);
+      logger.debug(`${this.logPrefix()} Trying to get MeterValues measurand ${measurand} ${phase ? `on phase ${phase} ` : ''}in template on connectorId ${connectorId} not found in '${StandardParametersKey.MeterValuesSampledData}' OCPP parameter`);
       return;
     }
     const sampledValueTemplates: SampledValueTemplate[] = this.getConnector(connectorId).MeterValues;
     for (let index = 0; !Utils.isEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length; index++) {
-      if (phase && sampledValueTemplates[index]?.phase === phase && sampledValueTemplates[index]?.measurand === measurand
+      if (!Constants.SUPPORTED_MEASURANDS.includes(sampledValueTemplates[index]?.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER)) {
+        logger.warn(`${this.logPrefix()} Unsupported MeterValues measurand ${measurand} ${phase ? `on phase ${phase} ` : ''}in template on connectorId ${connectorId}`);
+        continue;
+      } else if (phase && sampledValueTemplates[index]?.phase === phase && sampledValueTemplates[index]?.measurand === measurand
           && this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(measurand)) {
         return sampledValueTemplates[index];
       } else if (!phase && !sampledValueTemplates[index].phase && sampledValueTemplates[index]?.measurand === measurand
@@ -285,15 +291,7 @@ export default class ChargingStation {
     if (interval > 0) {
       // eslint-disable-next-line @typescript-eslint/no-misused-promises
       this.getConnector(connectorId).transactionSetInterval = setInterval(async (): Promise<void> => {
-        if (this.getEnableStatistics()) {
-          const sendMeterValues = performance.timerify(this.ocppRequestService.sendMeterValues);
-          this.performanceObserver.observe({
-            entryTypes: ['function'],
-          });
-          await sendMeterValues(connectorId, this.getConnector(connectorId).transactionId, interval, this.ocppRequestService);
-        } else {
-          await this.ocppRequestService.sendMeterValues(connectorId, this.getConnector(connectorId).transactionId, interval, this.ocppRequestService);
-        }
+        await this.ocppRequestService.sendMeterValues(connectorId, this.getConnector(connectorId).transactionId, interval);
       }, interval);
     } else {
       logger.error(`${this.logPrefix()} Charging station ${StandardParametersKey.MeterValueSampleInterval} configuration set to ${interval ? Utils.milliSecondsToHHMMSS(interval) : interval}, not sending MeterValues`);
@@ -301,6 +299,9 @@ export default class ChargingStation {
   }
 
   public start(): void {
+    if (this.getEnableStatistics()) {
+      this.performanceStatistics.start();
+    }
     this.openWSConnection();
     // Monitor authorization file
     this.startAuthorizationFileMonitoring();
@@ -332,18 +333,20 @@ export default class ChargingStation {
     if (this.isWebSocketOpen()) {
       this.wsConnection.close();
     }
+    if (this.getEnableStatistics()) {
+      this.performanceStatistics.stop();
+    }
     this.bootNotificationResponse = null;
     this.hasStopped = true;
   }
 
   public getConfigurationKey(key: string | StandardParametersKey, caseInsensitive = false): ConfigurationKey | undefined {
-    const configurationKey: ConfigurationKey | undefined = this.configuration.configurationKey.find((configElement) => {
+    return this.configuration.configurationKey.find((configElement) => {
       if (caseInsensitive) {
         return configElement.key.toLowerCase() === key.toLowerCase();
       }
       return configElement.key === key;
     });
-    return configurationKey;
   }
 
   public addConfigurationKey(key: string | StandardParametersKey, value: string, readonly = false, visible = true, reboot = false): void {
@@ -371,24 +374,26 @@ export default class ChargingStation {
     }
   }
 
-  public setChargingProfile(connectorId: number, cp: ChargingProfile): boolean {
+  public setChargingProfile(connectorId: number, cp: ChargingProfile): void {
+    let cpReplaced = false;
     if (!Utils.isEmptyArray(this.getConnector(connectorId).chargingProfiles)) {
       this.getConnector(connectorId).chargingProfiles?.forEach((chargingProfile: ChargingProfile, index: number) => {
         if (chargingProfile.chargingProfileId === cp.chargingProfileId
-          || (chargingProfile.stackLevel === cp.stackLevel && chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)) {
+            || (chargingProfile.stackLevel === cp.stackLevel && chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)) {
           this.getConnector(connectorId).chargingProfiles[index] = cp;
-          return true;
+          cpReplaced = true;
         }
       });
     }
-    this.getConnector(connectorId).chargingProfiles?.push(cp);
-    return true;
+    !cpReplaced && this.getConnector(connectorId).chargingProfiles?.push(cp);
   }
 
   public resetTransactionOnConnector(connectorId: number): void {
+    this.getConnector(connectorId).authorized = false;
     this.getConnector(connectorId).transactionStarted = false;
+    delete this.getConnector(connectorId).authorizeIdTag;
     delete this.getConnector(connectorId).transactionId;
-    delete this.getConnector(connectorId).idTag;
+    delete this.getConnector(connectorId).transactionIdTag;
     this.getConnector(connectorId).transactionEnergyActiveImportRegisterValue = 0;
     delete this.getConnector(connectorId).transactionBeginMeterValue;
     this.stopMeterValues(connectorId);
@@ -414,6 +419,7 @@ export default class ChargingStation {
     if (!Utils.isEmptyArray(this.messageQueue)) {
       this.messageQueue.forEach((message, index) => {
         this.messageQueue.splice(index, 1);
+        // TODO: evaluate the need to track performance
         this.wsConnection.send(message);
       });
     }
@@ -476,8 +482,7 @@ export default class ChargingStation {
       ...!Utils.isUndefined(this.stationInfo.firmwareVersion) && { firmwareVersion: this.stationInfo.firmwareVersion },
     };
     this.configuration = this.getTemplateChargingStationConfiguration();
-    this.supervisionUrl = this.getSupervisionURL();
-    this.wsConnectionUrl = this.supervisionUrl + '/' + this.stationInfo.chargingStationId;
+    this.wsConnectionUrl = new URL(this.getSupervisionURL().href + '/' + this.stationInfo.chargingStationId);
     // Build connectors if needed
     const maxConnectors = this.getMaxNumberOfConnectors();
     if (maxConnectors <= 0) {
@@ -513,8 +518,8 @@ export default class ChargingStation {
       // Generate all connectors
       if ((this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
         for (let index = 1; index <= maxConnectors; index++) {
-          const randConnectorID = this.stationInfo.randomConnectors ? Utils.getRandomInt(Utils.convertToInt(lastConnector), 1) : index;
-          this.connectors[index] = Utils.cloneObject<Connector>(this.stationInfo.Connectors[randConnectorID]);
+          const randConnectorId = this.stationInfo.randomConnectors ? Utils.getRandomInt(Utils.convertToInt(lastConnector), 1) : index;
+          this.connectors[index] = Utils.cloneObject<Connector>(this.stationInfo.Connectors[randConnectorId]);
           this.connectors[index].availability = AvailabilityType.OPERATIVE;
           if (Utils.isUndefined(this.connectors[lastConnector]?.chargingProfiles)) {
             this.connectors[index].chargingProfiles = [];
@@ -541,14 +546,16 @@ export default class ChargingStation {
     }
     // OCPP parameters
     this.initOCPPParameters();
+    if (this.stationInfo.autoRegister) {
+      this.bootNotificationResponse = {
+        currentTime: new Date().toISOString(),
+        interval: this.getHeartbeatInterval() / 1000,
+        status: RegistrationStatus.ACCEPTED
+      };
+    }
     this.stationInfo.powerDivider = this.getPowerDivider();
     if (this.getEnableStatistics()) {
       this.performanceStatistics = new PerformanceStatistics(this.stationInfo.chargingStationId);
-      this.performanceObserver = new PerformanceObserver((list) => {
-        const entry = list.getEntries()[0];
-        this.performanceStatistics.logPerformance(entry, Constants.ENTITY_CHARGING_STATION);
-        this.performanceObserver.disconnect();
-      });
     }
   }
 
@@ -590,7 +597,7 @@ export default class ChargingStation {
   }
 
   private async onOpen(): Promise<void> {
-    logger.info(`${this.logPrefix()} Is connected to server through ${this.wsConnectionUrl}`);
+    logger.info(`${this.logPrefix()} Connected to OCPP server through ${this.wsConnectionUrl.toString()}`);
     if (!this.isRegistered()) {
       // Send BootNotification
       let registrationRetryCount = 0;
@@ -637,14 +644,19 @@ export default class ChargingStation {
     let requestPayload: Record<string, unknown>;
     let errMsg: string;
     try {
-      // Parse the message
-      [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(messageEvent.toString()) as IncomingRequest;
+      const request = JSON.parse(messageEvent.toString()) as IncomingRequest;
+      if (Utils.isIterable(request)) {
+        // Parse the message
+        [messageType, messageId, commandName, commandPayload, errorDetails] = request;
+      } else {
+        throw new Error('Incoming request is not iterable');
+      }
       // Check the Type of message
       switch (messageType) {
         // Incoming Message
         case MessageType.CALL_MESSAGE:
           if (this.getEnableStatistics()) {
-            this.performanceStatistics.addMessage(commandName, messageType);
+            this.performanceStatistics.addRequestStatistic(commandName, messageType);
           }
           // Process the call
           await this.ocppIncomingRequestService.handleRequest(messageId, commandName, commandPayload);
@@ -686,18 +698,18 @@ export default class ChargingStation {
       }
     } catch (error) {
       // Log
-      logger.error('%s Incoming message %j processing error %j on request content type %j', this.logPrefix(), messageEvent, error, this.requests[messageId]);
+      logger.error('%s Incoming request message %j processing error %j on content type %j', this.logPrefix(), messageEvent, error, this.requests[messageId]);
       // Send error
       messageType !== MessageType.CALL_ERROR_MESSAGE && await this.ocppRequestService.sendError(messageId, error, commandName);
     }
   }
 
   private onPing(): void {
-    logger.debug(this.logPrefix() + ' Has received a WS ping (rfc6455) from the server');
+    logger.debug(this.logPrefix() + ' Received a WS ping (rfc6455) from the server');
   }
 
   private onPong(): void {
-    logger.debug(this.logPrefix() + ' Has received a WS pong (rfc6455) from the server');
+    logger.debug(this.logPrefix() + ' Received a WS pong (rfc6455) from the server');
   }
 
   private async onError(errorEvent: any): Promise<void> {
@@ -710,7 +722,7 @@ export default class ChargingStation {
   }
 
   private getTemplateChargingStationConfiguration(): ChargingStationConfiguration {
-    return this.stationInfo.Configuration ? this.stationInfo.Configuration : {} as ChargingStationConfiguration;
+    return this.stationInfo.Configuration ?? {} as ChargingStationConfiguration;
   }
 
   private getAuthorizationFile(): string | undefined {
@@ -834,9 +846,6 @@ export default class ChargingStation {
     }
     // Start the ATG
     this.startAutomaticTransactionGenerator();
-    if (this.getEnableStatistics()) {
-      this.performanceStatistics.start();
-    }
   }
 
   private startAutomaticTransactionGenerator() {
@@ -896,7 +905,7 @@ export default class ChargingStation {
     }
   }
 
-  private getSupervisionURL(): string {
+  private getSupervisionURL(): URL {
     const supervisionUrls = Utils.cloneObject<string | string[]>(this.stationInfo.supervisionURL ? this.stationInfo.supervisionURL : Configuration.getSupervisionURLs());
     let indexUrl = 0;
     if (!Utils.isEmptyArray(supervisionUrls)) {
@@ -906,9 +915,9 @@ export default class ChargingStation {
         // Get a random url
         indexUrl = Math.floor(Math.random() * supervisionUrls.length);
       }
-      return supervisionUrls[indexUrl];
+      return new URL(supervisionUrls[indexUrl]);
     }
-    return supervisionUrls as string;
+    return new URL(supervisionUrls as string);
   }
 
   private getHeartbeatInterval(): number | undefined {
@@ -920,6 +929,8 @@ export default class ChargingStation {
     if (HeartBeatInterval) {
       return Utils.convertToInt(HeartBeatInterval.value) * 1000;
     }
+    !this.stationInfo.autoRegister && logger.warn(`${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${Constants.DEFAULT_HEARTBEAT_INTERVAL}`);
+    return Constants.DEFAULT_HEARTBEAT_INTERVAL;
   }
 
   private stopHeartbeat(): void {
@@ -928,9 +939,12 @@ export default class ChargingStation {
     }
   }
 
-  private openWSConnection(options?: WebSocket.ClientOptions, forceCloseOpened = false): void {
-    options ?? {} as WebSocket.ClientOptions;
-    options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
+  private openWSConnection(options?: ClientOptions & ClientRequestArgs, forceCloseOpened = false): void {
+    options = options ?? {};
+    options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
+    if (!Utils.isNullOrUndefined(this.stationInfo.supervisionUser) && !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)) {
+      options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
+    }
     if (this.isWebSocketOpen() && forceCloseOpened) {
       this.wsConnection.close();
     }
@@ -944,7 +958,7 @@ export default class ChargingStation {
         break;
     }
     this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
-    logger.info(this.logPrefix() + ' Will communicate through URL ' + this.supervisionUrl);
+    logger.info(this.logPrefix() + ' Open OCPP connection to URL ' + this.wsConnectionUrl.toString());
   }
 
   private stopMeterValues(connectorId: number) {
@@ -982,13 +996,17 @@ export default class ChargingStation {
           logger.debug(this.logPrefix() + ' Template file ' + this.stationTemplateFile + ' have changed, reload');
           // Initialize
           this.initialize();
-          // Stop the ATG
+          // Restart the ATG
           if (!this.stationInfo.AutomaticTransactionGenerator.enable &&
           this.automaticTransactionGeneration) {
             await this.automaticTransactionGeneration.stop();
           }
-          // Start the ATG
           this.startAutomaticTransactionGenerator();
+          if (this.getEnableStatistics()) {
+            this.performanceStatistics.restart();
+          } else {
+            this.performanceStatistics.stop();
+          }
           // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
         } catch (error) {
           logger.error(this.logPrefix() + ' Charging station template file monitoring error: %j', error);
@@ -1004,6 +1022,8 @@ export default class ChargingStation {
   }
 
   private async reconnect(error: any): Promise<void> {
+    // Stop WebSocket ping
+    this.stopWebSocketPing();
     // Stop heartbeat
     this.stopHeartbeat();
     // Stop the ATG if needed
@@ -1027,6 +1047,7 @@ export default class ChargingStation {
   }
 
   private initTransactionAttributesOnConnector(connectorId: number): void {
+    this.getConnector(connectorId).authorized = false;
     this.getConnector(connectorId).transactionStarted = false;
     this.getConnector(connectorId).energyActiveImportRegisterValue = 0;
     this.getConnector(connectorId).transactionEnergyActiveImportRegisterValue = 0;