Fix merge.
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 6bd51d017899670b8071b4b45df6958e0b4e0cfa..ada1c4b9e19c743c7fbcbbee7ca5925d5aaf1a88 100644 (file)
@@ -1,11 +1,12 @@
 import { AuthorizationStatus, StartTransactionRequest, StartTransactionResponse, StopTransactionReason, StopTransactionRequest, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
-import { BootNotificationResponse, ChangeConfigurationResponse, DefaultResponse, GetConfigurationResponse, HeartbeatResponse, RegistrationStatus, StatusNotificationResponse, UnlockConnectorResponse } from '../types/ocpp/1.6/RequestResponses';
+import { BootNotificationResponse, ChangeConfigurationResponse, DefaultResponse, GetConfigurationResponse, HeartbeatResponse, RegistrationStatus, SetChargingProfileResponse, StatusNotificationResponse, UnlockConnectorResponse } from '../types/ocpp/1.6/RequestResponses';
+import { ChargingProfile, ChargingProfilePurposeType } from '../types/ocpp/1.6/ChargingProfile';
 import ChargingStationConfiguration, { ConfigurationKey } from '../types/ChargingStationConfiguration';
 import ChargingStationTemplate, { PowerOutType } from '../types/ChargingStationTemplate';
 import Connectors, { Connector } from '../types/Connectors';
 import { MeterValue, MeterValueLocation, MeterValueMeasurand, MeterValuePhase, MeterValueUnit, MeterValuesRequest, MeterValuesResponse, SampledValue } from '../types/ocpp/1.6/MeterValues';
 import { PerformanceObserver, performance } from 'perf_hooks';
-import Requests, { BootNotificationRequest, ChangeConfigurationRequest, GetConfigurationRequest, HeartbeatRequest, RemoteStartTransactionRequest, RemoteStopTransactionRequest, ResetRequest, StatusNotificationRequest, UnlockConnectorRequest } from '../types/ocpp/1.6/Requests';
+import Requests, { BootNotificationRequest, ChangeConfigurationRequest, GetConfigurationRequest, HeartbeatRequest, RemoteStartTransactionRequest, RemoteStopTransactionRequest, ResetRequest, SetChargingProfileRequest, StatusNotificationRequest, UnlockConnectorRequest } from '../types/ocpp/1.6/Requests';
 import WebSocket, { MessageEvent } from 'ws';
 
 import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
@@ -117,7 +118,10 @@ export default class ChargingStation {
     }
     const templateMaxConnectors = this._getTemplateMaxNumberOfConnectors();
     if (templateMaxConnectors <= 0) {
-      logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector configurations`);
+      logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector configuration`);
+    }
+    if (!this._stationInfo.Connectors[0]) {
+      logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector Id 0 configuration`);
     }
     // Sanity check
     if (maxConnectors > (this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) && !this._stationInfo.randomConnectors) {
@@ -131,7 +135,7 @@ export default class ChargingStation {
       // Add connector Id 0
       let lastConnector = '0';
       for (lastConnector in this._stationInfo.Connectors) {
-        if (Utils.convertToInt(lastConnector) === 0 && this._stationInfo.useConnectorId0 && this._stationInfo.Connectors[lastConnector]) {
+        if (Utils.convertToInt(lastConnector) === 0 && this._getUseConnectorId0() && this._stationInfo.Connectors[lastConnector]) {
           this._connectors[lastConnector] = Utils.cloneObject(this._stationInfo.Connectors[lastConnector]) as Connector;
         }
       }
@@ -147,7 +151,7 @@ export default class ChargingStation {
     delete this._stationInfo.Connectors;
     // Initialize transaction attributes on connectors
     for (const connector in this._connectors) {
-      if (!this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
+      if (Utils.convertToInt(connector) > 0 && !this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
         this._initTransactionOnConnector(Utils.convertToInt(connector));
       }
     }
@@ -188,6 +192,10 @@ export default class ChargingStation {
     return this._stationInfo.authorizationFile && this._stationInfo.authorizationFile;
   }
 
+  _getUseConnectorId0(): boolean {
+    return !Utils.isUndefined(this._stationInfo.useConnectorId0) ? this._stationInfo.useConnectorId0 : true;
+  }
+
   _loadAndGetAuthorizedTags(): string[] {
     let authorizedTags: string[] = [];
     const authorizationFile = this._getAuthorizationFile();
@@ -223,7 +231,7 @@ export default class ChargingStation {
   _getNumberOfPhases(): number {
     switch (this._getPowerOutType()) {
       case PowerOutType.AC:
-        return !Utils.isUndefined(this._stationInfo.numberOfPhases) ? Utils.convertToInt(this._stationInfo.numberOfPhases) : 3;
+        return !Utils.isUndefined(this._stationInfo.numberOfPhases) ? this._stationInfo.numberOfPhases : 3;
       case PowerOutType.DC:
         return 0;
     }
@@ -232,7 +240,7 @@ export default class ChargingStation {
   _getNumberOfRunningTransactions(): number {
     let trxCount = 0;
     for (const connector in this._connectors) {
-      if (this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
+      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
         trxCount++;
       }
     }
@@ -307,12 +315,12 @@ export default class ChargingStation {
         logger.error(errMsg);
         throw Error(errMsg);
     }
-    return !Utils.isUndefined(this._stationInfo.voltageOut) ? Utils.convertToInt(this._stationInfo.voltageOut) : defaultVoltageOut;
+    return !Utils.isUndefined(this._stationInfo.voltageOut) ? this._stationInfo.voltageOut : defaultVoltageOut;
   }
 
   _getTransactionIdTag(transactionId: number): string {
     for (const connector in this._connectors) {
-      if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
+      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
         return this.getConnector(Utils.convertToInt(connector)).idTag;
       }
     }
@@ -320,7 +328,7 @@ export default class ChargingStation {
 
   _getTransactionMeterStop(transactionId: number): number {
     for (const connector in this._connectors) {
-      if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
+      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
         return this.getConnector(Utils.convertToInt(connector)).lastEnergyActiveImportRegisterValue;
       }
     }
@@ -366,7 +374,9 @@ export default class ChargingStation {
     this._startHeartbeat();
     // Initialize connectors status
     for (const connector in this._connectors) {
-      if (!this._hasStopped && !this.getConnector(Utils.convertToInt(connector)).status && this.getConnector(Utils.convertToInt(connector)).bootStatus) {
+      if (Utils.convertToInt(connector) === 0) {
+        continue;
+      } else if (!this._hasStopped && !this.getConnector(Utils.convertToInt(connector)).status && this.getConnector(Utils.convertToInt(connector)).bootStatus) {
         // Send status in template at startup
         await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
       } else if (this._hasStopped && this.getConnector(Utils.convertToInt(connector)).bootStatus) {
@@ -406,7 +416,7 @@ export default class ChargingStation {
       await this._automaticTransactionGeneration.stop(reason);
     } else {
       for (const connector in this._connectors) {
-        if (this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
+        if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
           await this.sendStopTransaction(this.getConnector(Utils.convertToInt(connector)).transactionId, reason);
         }
       }
@@ -526,13 +536,16 @@ export default class ChargingStation {
     }
   }
 
-  _openWSConnection(options?: WebSocket.ClientOptions): void {
+  _openWSConnection(options?: WebSocket.ClientOptions, forceCloseOpened = false): void {
     if (Utils.isUndefined(options)) {
       options = {} as WebSocket.ClientOptions;
     }
     if (Utils.isUndefined(options.handshakeTimeout)) {
       options.handshakeTimeout = this._connectionTimeout;
     }
+    if (this._wsConnection?.readyState === WebSocket.OPEN && forceCloseOpened) {
+      this._wsConnection.close();
+    }
     this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16, options);
     logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl);
   }
@@ -560,9 +573,10 @@ export default class ChargingStation {
   async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
     // Stop message sequence
     await this._stopMessageSequence(reason);
-    // eslint-disable-next-line guard-for-in
     for (const connector in this._connectors) {
-      await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE);
+      if (Utils.convertToInt(connector) > 0) {
+        await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE);
+      }
     }
     if (this._wsConnection?.readyState === WebSocket.OPEN) {
       this._wsConnection.close();
@@ -589,6 +603,7 @@ export default class ChargingStation {
       await Utils.sleep(reconnectDelay);
       logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount.toString());
       this._openWSConnection({ handshakeTimeout: reconnectDelay - 100 });
+      this._hasSocketRestarted = true;
     } else if (this._autoReconnectMaxRetries !== -1) {
       logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectMaxRetries})`);
     }
@@ -626,7 +641,6 @@ export default class ChargingStation {
   async onError(errorEvent): Promise<void> {
     switch (errorEvent.code) {
       case 'ECONNREFUSED':
-        this._hasSocketRestarted = true;
         await this._reconnect(errorEvent);
         break;
       default:
@@ -643,7 +657,6 @@ export default class ChargingStation {
         this._autoReconnectRetryCount = 0;
         break;
       default: // Abnormal close
-        this._hasSocketRestarted = true;
         await this._reconnect(closeEvent);
         break;
     }
@@ -821,11 +834,10 @@ export default class ChargingStation {
             ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() },
           });
           for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
-            const voltageValue = Utils.convertToFloat(meterValue.sampledValue[meterValue.sampledValue.length - 1].value);
             let phaseValue: string;
-            if (voltageValue >= 0 && voltageValue <= 250) {
+            if (self._getVoltageOut() >= 0 && self._getVoltageOut() <= 250) {
               phaseValue = `L${phase}-N`;
-            } else if (voltageValue > 250) {
+            } else if (self._getVoltageOut() > 250) {
               phaseValue = `L${phase}-L${(phase + 1) % self._getNumberOfPhases() !== 0 ? (phase + 1) % self._getNumberOfPhases() : self._getNumberOfPhases()}`;
             }
             meterValue.sampledValue.push({
@@ -1075,12 +1087,12 @@ export default class ChargingStation {
       }
 
       // Function that will receive the request's response
-      function responseCallback(payload, requestPayload): void {
+      async function responseCallback(payload, requestPayload): Promise<void> {
         if (self.getEnableStatistics()) {
           self._statistics.addMessage(commandName, messageType);
         }
         // Send the response
-        self.handleResponse(commandName, payload, requestPayload);
+        await self.handleResponse(commandName, payload, requestPayload);
         resolve(payload);
       }
 
@@ -1099,10 +1111,10 @@ export default class ChargingStation {
     });
   }
 
-  handleResponse(commandName: string, payload, requestPayload): void {
+  async handleResponse(commandName: string, payload, requestPayload): Promise<void> {
     const responseCallbackFn = 'handleResponse' + commandName;
     if (typeof this[responseCallbackFn] === 'function') {
-      this[responseCallbackFn](payload, requestPayload);
+      await this[responseCallbackFn](payload, requestPayload);
     } else {
       logger.error(this._logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn);
     }
@@ -1110,7 +1122,7 @@ export default class ChargingStation {
 
   handleResponseBootNotification(payload: BootNotificationResponse, requestPayload: BootNotificationRequest): void {
     if (payload.status === RegistrationStatus.ACCEPTED) {
-      this._heartbeatInterval = Utils.convertToInt(payload.interval) * 1000;
+      this._heartbeatInterval = payload.interval * 1000;
       this._heartbeatSetInterval ? this._restartHeartbeat() : this._startHeartbeat();
       this._addConfigurationKey('HeartBeatInterval', payload.interval.toString());
       this._addConfigurationKey('HeartbeatInterval', payload.interval.toString(), false, false);
@@ -1136,8 +1148,8 @@ export default class ChargingStation {
     }
   }
 
-  handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload: StartTransactionRequest): void {
-    const connectorId = Utils.convertToInt(requestPayload.connectorId);
+  async handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload: StartTransactionRequest): Promise<void> {
+    const connectorId = requestPayload.connectorId;
     if (this.getConnector(connectorId).transactionStarted) {
       logger.debug(this._logPrefix() + ' Trying to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.getConnector(connectorId));
       return;
@@ -1145,7 +1157,7 @@ export default class ChargingStation {
 
     let transactionConnectorId: number;
     for (const connector in this._connectors) {
-      if (Utils.convertToInt(connector) === connectorId) {
+      if (Utils.convertToInt(connector) > 0 && Utils.convertToInt(connector) === connectorId) {
         transactionConnectorId = Utils.convertToInt(connector);
         break;
       }
@@ -1159,7 +1171,7 @@ export default class ChargingStation {
       this.getConnector(connectorId).transactionId = payload.transactionId;
       this.getConnector(connectorId).idTag = requestPayload.idTag;
       this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = 0;
-      this.sendStatusNotification(connectorId, ChargePointStatus.CHARGING).catch(() => { });
+      await this.sendStatusNotification(connectorId, ChargePointStatus.CHARGING);
       logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId.toString() + ' STARTED on ' + this._stationInfo.name + '#' + connectorId.toString() + ' for idTag ' + requestPayload.idTag);
       if (this._stationInfo.powerSharedByConnectors) {
         this._stationInfo.powerDivider++;
@@ -1170,14 +1182,14 @@ export default class ChargingStation {
     } else {
       logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag);
       this._resetTransactionOnConnector(connectorId);
-      this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE).catch(() => { });
+      await this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE);
     }
   }
 
-  handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload: StopTransactionRequest): void {
+  async handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload: StopTransactionRequest): Promise<void> {
     let transactionConnectorId: number;
     for (const connector in this._connectors) {
-      if (this.getConnector(Utils.convertToInt(connector)).transactionId === Utils.convertToInt(requestPayload.transactionId)) {
+      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === requestPayload.transactionId) {
         transactionConnectorId = Utils.convertToInt(connector);
         break;
       }
@@ -1187,7 +1199,7 @@ export default class ChargingStation {
       return;
     }
     if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
-      this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE).catch(() => { });
+      await this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE);
       if (this._stationInfo.powerSharedByConnectors) {
         this._stationInfo.powerDivider--;
       }
@@ -1249,7 +1261,7 @@ export default class ChargingStation {
   }
 
   async handleRequestUnlockConnector(commandPayload: UnlockConnectorRequest): Promise<UnlockConnectorResponse> {
-    const connectorId = Utils.convertToInt(commandPayload.connectorId);
+    const connectorId = commandPayload.connectorId;
     if (connectorId === 0) {
       logger.error(this._logPrefix() + ' Trying to unlock connector ' + connectorId.toString());
       return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
@@ -1334,6 +1346,13 @@ export default class ChargingStation {
   }
 
   handleRequestChangeConfiguration(commandPayload: ChangeConfigurationRequest): ChangeConfigurationResponse {
+    // JSON request fields type sanity check
+    if (!Utils.isString(commandPayload.key)) {
+      logger.error(`${this._logPrefix()} ChangeConfiguration request key field is not a string:`, commandPayload);
+    }
+    if (!Utils.isString(commandPayload.value)) {
+      logger.error(`${this._logPrefix()} ChangeConfiguration request value field is not a string:`, commandPayload);
+    }
     const keyToChange = this._getConfigurationKey(commandPayload.key);
     if (!keyToChange) {
       return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED;
@@ -1369,8 +1388,27 @@ export default class ChargingStation {
     }
   }
 
+  handleRequestSetChargingProfile(commandPayload: SetChargingProfileRequest): SetChargingProfileResponse {
+    if (!this.getConnector(commandPayload.connectorId)) {
+      logger.error(`${this._logPrefix()} Trying to set a charging profile to a non existing connector Id ${commandPayload.connectorId}`);
+      return Constants.OCPP_CHARGING_PROFILE_RESPONSE_REJECTED;
+    }
+    if (commandPayload.csChargingProfiles.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE && !this.getConnector(commandPayload.connectorId)?.transactionStarted) {
+      return Constants.OCPP_CHARGING_PROFILE_RESPONSE_REJECTED;
+    }
+    this.getConnector(commandPayload.connectorId).chargingProfiles.forEach((chargingProfile: ChargingProfile, index: number) => {
+      if (chargingProfile.chargingProfileId === commandPayload.csChargingProfiles.chargingProfileId
+        || (chargingProfile.stackLevel === commandPayload.csChargingProfiles.stackLevel && chargingProfile.chargingProfilePurpose === commandPayload.csChargingProfiles.chargingProfilePurpose)) {
+        this.getConnector(commandPayload.connectorId).chargingProfiles[index] = chargingProfile;
+        return Constants.OCPP_CHARGING_PROFILE_RESPONSE_ACCEPTED;
+      }
+    });
+    this.getConnector(commandPayload.connectorId).chargingProfiles.push(commandPayload.csChargingProfiles);
+    return Constants.OCPP_CHARGING_PROFILE_RESPONSE_ACCEPTED;
+  }
+
   async handleRequestRemoteStartTransaction(commandPayload: RemoteStartTransactionRequest): Promise<DefaultResponse> {
-    const transactionConnectorID: number = commandPayload.connectorId ? Utils.convertToInt(commandPayload.connectorId) : 1;
+    const transactionConnectorID: number = commandPayload.connectorId ? commandPayload.connectorId : 1;
     if (this._getAuthorizeRemoteTxRequests() && this._getLocalAuthListEnabled() && this.hasAuthorizedTags()) {
       // Check if authorized
       if (this._authorizedTags.find((value) => value === commandPayload.idTag)) {
@@ -1389,9 +1427,9 @@ export default class ChargingStation {
   }
 
   async handleRequestRemoteStopTransaction(commandPayload: RemoteStopTransactionRequest): Promise<DefaultResponse> {
-    const transactionId = Utils.convertToInt(commandPayload.transactionId);
+    const transactionId = commandPayload.transactionId;
     for (const connector in this._connectors) {
-      if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
+      if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
         await this.sendStopTransaction(transactionId);
         return Constants.OCPP_RESPONSE_ACCEPTED;
       }