Fix worker startup and cleanup some attributes in Wrk class.
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index cdf87cddc903cb173abd3492dfacf1bfdf8057db..8341fe30a71612a1b52ff5da90ca5245dde53d09 100644 (file)
@@ -1,9 +1,9 @@
-import { AuthorizationStatus, StartTransactionRequest, StartTransactionResponse, StopTransactionReason, StopTransactionRequest, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
+import { AuthorizationStatus, AuthorizeRequest, AuthorizeResponse, StartTransactionRequest, StartTransactionResponse, StopTransactionReason, StopTransactionRequest, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
 import { AvailabilityType, BootNotificationRequest, ChangeAvailabilityRequest, ChangeConfigurationRequest, GetConfigurationRequest, HeartbeatRequest, IncomingRequestCommand, RemoteStartTransactionRequest, RemoteStopTransactionRequest, RequestCommand, ResetRequest, SetChargingProfileRequest, StatusNotificationRequest, UnlockConnectorRequest } from '../types/ocpp/1.6/Requests';
 import { BootNotificationResponse, ChangeAvailabilityResponse, 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 ChargingStationTemplate, { PowerOutType, VoltageOut } 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';
@@ -71,7 +71,13 @@ export default class ChargingStation {
   }
 
   _getStationName(stationTemplate: ChargingStationTemplate): string {
-    return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + ('000000000' + this._index.toString()).substr(('000000000' + this._index.toString()).length - 4);
+    // In case of multiple instances: add instance index to charging station id
+    let instanceIndex = process.env.CF_INSTANCE_INDEX ? process.env.CF_INSTANCE_INDEX : 0;
+    instanceIndex = instanceIndex > 0 ? instanceIndex : '';
+
+    const idSuffix = stationTemplate.nameSuffix ? stationTemplate.nameSuffix : '';
+
+    return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + instanceIndex.toString() + ('000000000' + this._index.toString()).substr(('000000000' + this._index.toString()).length - 4) + idSuffix;
   }
 
   _buildStationInfo(): ChargingStationInfo {
@@ -304,6 +310,10 @@ export default class ChargingStation {
     return this.getConnector(id).availability === AvailabilityType.OPERATIVE;
   }
 
+  _isChargingStationAvailable(): boolean {
+    return this.getConnector(0).availability === AvailabilityType.OPERATIVE;
+  }
+
   _getTemplateMaxNumberOfConnectors(): number {
     return Object.keys(this._stationInfo.Connectors).length;
   }
@@ -331,10 +341,10 @@ export default class ChargingStation {
     let defaultVoltageOut: number;
     switch (this._getPowerOutType()) {
       case PowerOutType.AC:
-        defaultVoltageOut = 230;
+        defaultVoltageOut = VoltageOut.VOLTAGE_230;
         break;
       case PowerOutType.DC:
-        defaultVoltageOut = 400;
+        defaultVoltageOut = VoltageOut.VOLTAGE_400;
         break;
       default:
         logger.error(errMsg);
@@ -517,8 +527,7 @@ export default class ChargingStation {
   }
 
   _startAuthorizationFileMonitoring(): void {
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    fs.watchFile(this._getAuthorizationFile(), (current, previous) => {
+    fs.watch(this._getAuthorizationFile()).on('change', (e) => {
       try {
         logger.debug(this._logPrefix() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload');
         // Initialize _authorizedTags
@@ -530,16 +539,25 @@ export default class ChargingStation {
   }
 
   _startStationTemplateFileMonitoring(): void {
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
-    fs.watchFile(this._stationTemplateFile, (current, previous) => {
+    fs.watch(this._stationTemplateFile).on('change', (e) => {
       try {
         logger.debug(this._logPrefix() + ' Template file ' + this._stationTemplateFile + ' have changed, reload');
         // Initialize
         this._initialize();
+        // Stop the ATG
         if (!this._stationInfo.AutomaticTransactionGenerator.enable &&
           this._automaticTransactionGeneration) {
           this._automaticTransactionGeneration.stop().catch(() => { });
         }
+        // Start the ATG
+        if (this._stationInfo.AutomaticTransactionGenerator.enable) {
+          if (!this._automaticTransactionGeneration) {
+            this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
+          }
+          if (this._automaticTransactionGeneration.timeToStop) {
+            this._automaticTransactionGeneration.start();
+          }
+        }
         // 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);
@@ -576,7 +594,7 @@ export default class ChargingStation {
         }
       }, interval);
     } else {
-      logger.error(`${this._logPrefix()} Charging station MeterValueSampleInterval configuration set to ${Utils.milliSecondsToHHMMSS(interval)}, not sending MeterValues`);
+      logger.error(`${this._logPrefix()} Charging station ${StandardParametersKey.MeterValueSampleInterval} configuration set to ${Utils.milliSecondsToHHMMSS(interval)}, not sending MeterValues`);
     }
   }
 
@@ -661,7 +679,7 @@ export default class ChargingStation {
         this._bootNotificationResponse = await this.sendBootNotification();
         if (!this._isRegistered()) {
           registrationRetryCount++;
-          await Utils.sleep(this._bootNotificationResponse.interval * 1000);
+          await Utils.sleep(this._bootNotificationResponse?.interval ? this._bootNotificationResponse.interval * 1000 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL);
         }
       } while (!this._isRegistered() && (registrationRetryCount <= this._getRegistrationMaxRetries() || this._getRegistrationMaxRetries() === -1));
     }
@@ -714,7 +732,7 @@ export default class ChargingStation {
   }
 
   async onMessage(messageEvent: MessageEvent): Promise<void> {
-    let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [0, '', '' as IncomingRequestCommand, '', {}];
+    let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [0, '', '' as IncomingRequestCommand, {}, {}];
     let responseCallback: (payload?: Record<string, unknown> | string, requestPayload?: Record<string, unknown>) => void;
     let rejectCallback: (error: OCPPError) => void;
     let requestPayload: Record<string, unknown>;
@@ -746,7 +764,7 @@ export default class ChargingStation {
             throw new Error(`Response request for unknown message id ${messageId}`);
           }
           delete this._requests[messageId];
-          responseCallback(commandName.toString(), requestPayload);
+          responseCallback(commandName, requestPayload);
           break;
         // Error Message
         case MessageType.CALL_ERROR_MESSAGE:
@@ -807,11 +825,22 @@ export default class ChargingStation {
     }
   }
 
+  async sendAuthorize(idTag?: string): Promise<AuthorizeResponse> {
+    try {
+      const payload: AuthorizeRequest = {
+        ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_TAGID },
+      };
+      return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.AUTHORIZE) as AuthorizeResponse;
+    } catch (error) {
+      this.handleRequestError(RequestCommand.AUTHORIZE, error);
+    }
+  }
+
   async sendStartTransaction(connectorId: number, idTag?: string): Promise<StartTransactionResponse> {
     try {
       const payload: StartTransactionRequest = {
         connectorId,
-        ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_IDTAG },
+        ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_TAGID },
         meterStart: 0,
         timestamp: new Date().toISOString(),
       };
@@ -874,7 +903,7 @@ export default class ChargingStation {
         }
         // Yes: Send Message
         this._wsConnection.send(messageToSend);
-      } else {
+      } else if (commandName !== RequestCommand.BOOT_NOTIFICATION) {
         let dups = false;
         // Handle dups in buffer
         for (const message of this._messageQueue) {
@@ -901,7 +930,7 @@ export default class ChargingStation {
       }
 
       // Function that will receive the request's response
-      async function responseCallback(payload, requestPayload): Promise<void> {
+      async function responseCallback(payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>): Promise<void> {
         if (self.getEnableStatistics()) {
           self._statistics.addMessage(commandName, messageType);
         }
@@ -925,7 +954,7 @@ export default class ChargingStation {
     });
   }
 
-  async handleResponse(commandName: RequestCommand, payload: Record<string, unknown>, requestPayload: Record<string, unknown>): Promise<void> {
+  async handleResponse(commandName: RequestCommand, payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>): Promise<void> {
     const responseCallbackFn = 'handleResponse' + commandName;
     if (typeof this[responseCallbackFn] === 'function') {
       await this[responseCallbackFn](payload, requestPayload);
@@ -980,7 +1009,7 @@ export default class ChargingStation {
       return;
     }
 
-    if (payload.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
+    if (payload?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
       this.getConnector(connectorId).transactionStarted = true;
       this.getConnector(connectorId).transactionId = payload.transactionId;
       this.getConnector(connectorId).idTag = requestPayload.idTag;
@@ -994,7 +1023,7 @@ export default class ChargingStation {
       this._startMeterValues(connectorId,
         configuredMeterValueSampleInterval ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000 : 60000);
     } else {
-      logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag);
+      logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload?.idTagInfo?.status + ', idTag ' + requestPayload.idTag);
       this._resetTransactionOnConnector(connectorId);
       await this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE);
     }
@@ -1013,7 +1042,11 @@ export default class ChargingStation {
       return;
     }
     if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
-      await this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE);
+      if (!this._isChargingStationAvailable() || !this._isConnectorAvailable(transactionConnectorId)) {
+        await this.sendStatusNotification(transactionConnectorId, ChargePointStatus.UNAVAILABLE);
+      } else {
+        await this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE);
+      }
       if (this._stationInfo.powerSharedByConnectors) {
         this._stationInfo.powerDivider--;
       }
@@ -1036,7 +1069,7 @@ export default class ChargingStation {
     logger.debug(this._logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload);
   }
 
-  async handleRequest(messageId: string, commandName: IncomingRequestCommand, commandPayload: Record<string, unknown> | string): Promise<void> {
+  async handleRequest(messageId: string, commandName: IncomingRequestCommand, commandPayload: Record<string, unknown>): Promise<void> {
     let response;
     // Call
     if (typeof this['handleRequest' + commandName] === 'function') {
@@ -1230,7 +1263,6 @@ export default class ChargingStation {
     return Constants.OCPP_CHARGING_PROFILE_RESPONSE_ACCEPTED;
   }
 
-  // FIXME: Handle properly the transaction started case
   handleRequestChangeAvailability(commandPayload: ChangeAvailabilityRequest): ChangeAvailabilityResponse {
     const connectorId: number = commandPayload.connectorId;
     if (!this.getConnector(connectorId)) {
@@ -1245,13 +1277,12 @@ export default class ChargingStation {
           response = Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
         }
         this.getConnector(Utils.convertToInt(connector)).availability = commandPayload.type;
-        void this.sendStatusNotification(Utils.convertToInt(connector), chargePointStatus);
+        response === Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED && this.sendStatusNotification(Utils.convertToInt(connector), chargePointStatus);
       }
       return response;
     } else if (connectorId > 0 && (this.getConnector(0).availability === AvailabilityType.OPERATIVE || (this.getConnector(0).availability === AvailabilityType.INOPERATIVE && commandPayload.type === AvailabilityType.INOPERATIVE))) {
       if (this.getConnector(connectorId)?.transactionStarted) {
         this.getConnector(connectorId).availability = commandPayload.type;
-        void this.sendStatusNotification(connectorId, chargePointStatus);
         return Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
       }
       this.getConnector(connectorId).availability = commandPayload.type;
@@ -1263,23 +1294,27 @@ export default class ChargingStation {
 
   async handleRequestRemoteStartTransaction(commandPayload: RemoteStartTransactionRequest): Promise<DefaultResponse> {
     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)) {
-        await this.sendStatusNotification(transactionConnectorID, ChargePointStatus.PREPARING);
-        // Authorization successful start transaction
-        await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
-        logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
-        return Constants.OCPP_RESPONSE_ACCEPTED;
+    if (this._isChargingStationAvailable() && this._isConnectorAvailable(transactionConnectorID)) {
+      if (this._getAuthorizeRemoteTxRequests() && this._getLocalAuthListEnabled() && this.hasAuthorizedTags()) {
+        // Check if authorized
+        if (this._authorizedTags.find((value) => value === commandPayload.idTag)) {
+          await this.sendStatusNotification(transactionConnectorID, ChargePointStatus.PREPARING);
+          // Authorization successful start transaction
+          await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
+          logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
+          return Constants.OCPP_RESPONSE_ACCEPTED;
+        }
+        logger.error(this._logPrefix() + ' Remote starting transaction REJECTED on connector Id ' + transactionConnectorID.toString() + ', idTag ' + commandPayload.idTag);
+        return Constants.OCPP_RESPONSE_REJECTED;
       }
-      logger.error(this._logPrefix() + ' Remote starting transaction REJECTED, idTag ' + commandPayload.idTag);
-      return Constants.OCPP_RESPONSE_REJECTED;
+      await this.sendStatusNotification(transactionConnectorID, ChargePointStatus.PREPARING);
+      // No local authorization check required => start transaction
+      await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
+      logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
+      return Constants.OCPP_RESPONSE_ACCEPTED;
     }
-    await this.sendStatusNotification(transactionConnectorID, ChargePointStatus.PREPARING);
-    // No local authorization check required => start transaction
-    await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
-    logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
-    return Constants.OCPP_RESPONSE_ACCEPTED;
+    logger.error(this._logPrefix() + ' Remote starting transaction REJECTED on unavailable connector Id ' + transactionConnectorID.toString() + ', idTag ' + commandPayload.idTag);
+    return Constants.OCPP_RESPONSE_REJECTED;
   }
 
   async handleRequestRemoteStopTransaction(commandPayload: RemoteStopTransactionRequest): Promise<DefaultResponse> {