Cleanups: performance statistics, URI handling.
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCCP16IncomingRequestService.ts
index 14a8b97d928df3e29f98ea884a1909517f85347d..40f80a2c50e398edb36adcb8b242f4d0b136ac80 100644 (file)
@@ -1,6 +1,10 @@
-import { ChangeAvailabilityRequest, ChangeConfigurationRequest, ClearChargingProfileRequest, GetConfigurationRequest, OCPP16AvailabilityType, OCPP16IncomingRequestCommand, RemoteStartTransactionRequest, RemoteStopTransactionRequest, ResetRequest, SetChargingProfileRequest, UnlockConnectorRequest } from '../../../types/ocpp/1.6/Requests';
-import { ChangeAvailabilityResponse, ChangeConfigurationResponse, ClearChargingProfileResponse, GetConfigurationResponse, SetChargingProfileResponse, UnlockConnectorResponse } from '../../../types/ocpp/1.6/Responses';
+import * as url from 'url';
+
+import { ChangeAvailabilityRequest, ChangeConfigurationRequest, ClearChargingProfileRequest, GetConfigurationRequest, GetDiagnosticsRequest, OCPP16AvailabilityType, OCPP16IncomingRequestCommand, RemoteStartTransactionRequest, RemoteStopTransactionRequest, ResetRequest, SetChargingProfileRequest, UnlockConnectorRequest } from '../../../types/ocpp/1.6/Requests';
+import { ChangeAvailabilityResponse, ChangeConfigurationResponse, ClearChargingProfileResponse, GetConfigurationResponse, GetDiagnosticsResponse, SetChargingProfileResponse, UnlockConnectorResponse } from '../../../types/ocpp/1.6/Responses';
 import { ChargingProfilePurposeType, OCPP16ChargingProfile } from '../../../types/ocpp/1.6/ChargingProfile';
+import { Client, FTPResponse } from 'basic-ftp';
+import { IncomingRequestCommand, RequestCommand } from '../../../types/ocpp/Requests';
 import { OCPP16AuthorizationStatus, OCPP16StopTransactionReason } from '../../../types/ocpp/1.6/Transaction';
 
 import Constants from '../../../utils/Constants';
@@ -8,12 +12,16 @@ import { DefaultResponse } from '../../../types/ocpp/Responses';
 import { ErrorType } from '../../../types/ocpp/ErrorType';
 import { MessageType } from '../../../types/ocpp/MessageType';
 import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus';
+import { OCPP16DiagnosticsStatus } from '../../../types/ocpp/1.6/DiagnosticsStatus';
 import { OCPP16StandardParametersKey } from '../../../types/ocpp/1.6/Configuration';
 import { OCPPConfigurationKey } from '../../../types/ocpp/Configuration';
-import OCPPError from '../../OcppError';
+import OCPPError from '../../OCPPError';
 import OCPPIncomingRequestService from '../OCPPIncomingRequestService';
 import Utils from '../../../utils/Utils';
+import fs from 'fs';
 import logger from '../../../utils/Logger';
+import path from 'path';
+import tar from 'tar';
 
 export default class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
   public async handleRequest(messageId: string, commandName: OCPP16IncomingRequestCommand, commandPayload: Record<string, unknown>): Promise<void> {
@@ -124,10 +132,10 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
   private handleRequestChangeConfiguration(commandPayload: ChangeConfigurationRequest): ChangeConfigurationResponse {
     // JSON request fields type sanity check
     if (!Utils.isString(commandPayload.key)) {
-      logger.error(`${this.chargingStation.logPrefix()} ChangeConfiguration request key field is not a string:`, commandPayload);
+      logger.error(`${this.chargingStation.logPrefix()} ${RequestCommand.CHANGE_CONFIGURATION} request key field is not a string:`, commandPayload);
     }
     if (!Utils.isString(commandPayload.value)) {
-      logger.error(`${this.chargingStation.logPrefix()} ChangeConfiguration request value field is not a string:`, commandPayload);
+      logger.error(`${this.chargingStation.logPrefix()} ${RequestCommand.CHANGE_CONFIGURATION} request value field is not a string:`, commandPayload);
     }
     const keyToChange = this.chargingStation.getConfigurationKey(commandPayload.key, true);
     if (!keyToChange) {
@@ -261,9 +269,9 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
       await this.chargingStation.ocppRequestService.sendStatusNotification(transactionConnectorId, OCPP16ChargePointStatus.PREPARING);
       this.chargingStation.getConnector(transactionConnectorId).status = OCPP16ChargePointStatus.PREPARING;
       if (this.chargingStation.isChargingStationAvailable() && this.chargingStation.isConnectorAvailable(transactionConnectorId)) {
+        // Check if authorized
         if (this.chargingStation.getAuthorizeRemoteTxRequests()) {
           let authorized = false;
-          // Check if authorized
           if (this.chargingStation.getLocalAuthListEnabled() && this.chargingStation.hasAuthorizedTags()
               && this.chargingStation.authorizedTags.find((value) => value === commandPayload.idTag)) {
             authorized = true;
@@ -278,43 +286,51 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
           }
           if (authorized) {
             // Authorization successful, start transaction
-            await this.chargingStation.ocppRequestService.sendStartTransaction(transactionConnectorId, commandPayload.idTag);
-            logger.debug(this.chargingStation.logPrefix() + ' Transaction remotely STARTED on ' + this.chargingStation.stationInfo.chargingStationId + '#' + transactionConnectorId.toString() + ' for idTag ' + commandPayload.idTag);
-            return await this.setRemoteStartChargingProfile(transactionConnectorId, commandPayload.chargingProfile)
-              ? Constants.OCPP_RESPONSE_ACCEPTED
-              : await this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
+            if (this.setRemoteStartTransactionChargingProfile(transactionConnectorId, commandPayload.chargingProfile)) {
+              if ((await this.chargingStation.ocppRequestService.sendStartTransaction(transactionConnectorId, commandPayload.idTag)).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED) {
+                logger.debug(this.chargingStation.logPrefix() + ' Transaction remotely STARTED on ' + this.chargingStation.stationInfo.chargingStationId + '#' + transactionConnectorId.toString() + ' for idTag ' + commandPayload.idTag);
+                return Constants.OCPP_RESPONSE_ACCEPTED;
+              }
+              return this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
+            }
+            return this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
           }
-          return await this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
+          return this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
         }
         // No authorization check required, start transaction
-        await this.chargingStation.ocppRequestService.sendStartTransaction(transactionConnectorId, commandPayload.idTag);
-        logger.debug(this.chargingStation.logPrefix() + ' Transaction remotely STARTED on ' + this.chargingStation.stationInfo.chargingStationId + '#' + transactionConnectorId.toString() + ' for idTag ' + commandPayload.idTag);
-        return await this.setRemoteStartChargingProfile(transactionConnectorId, commandPayload.chargingProfile)
-          ? Constants.OCPP_RESPONSE_ACCEPTED
-          : await this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
+        if (this.setRemoteStartTransactionChargingProfile(transactionConnectorId, commandPayload.chargingProfile)) {
+          if ((await this.chargingStation.ocppRequestService.sendStartTransaction(transactionConnectorId, commandPayload.idTag)).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED) {
+            logger.debug(this.chargingStation.logPrefix() + ' Transaction remotely STARTED on ' + this.chargingStation.stationInfo.chargingStationId + '#' + transactionConnectorId.toString() + ' for idTag ' + commandPayload.idTag);
+            return Constants.OCPP_RESPONSE_ACCEPTED;
+          }
+          return this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
+        }
+        return this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
       }
-      return await this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
+      return this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
     }
-    return await this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
+    return this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
   }
 
   private async notifyRemoteStartTransactionRejected(connectorId: number, idTag: string): Promise<DefaultResponse> {
-    await this.chargingStation.ocppRequestService.sendStatusNotification(connectorId, OCPP16ChargePointStatus.AVAILABLE);
-    this.chargingStation.getConnector(connectorId).status = OCPP16ChargePointStatus.AVAILABLE;
-    logger.warn(this.chargingStation.logPrefix() + ' Remote starting transaction REJECTED on connector Id ' + connectorId.toString() + ', availability ' + this.chargingStation.getConnector(connectorId).availability + ', idTag ' + idTag);
+    if (this.chargingStation.getConnector(connectorId).status !== OCPP16ChargePointStatus.AVAILABLE) {
+      await this.chargingStation.ocppRequestService.sendStatusNotification(connectorId, OCPP16ChargePointStatus.AVAILABLE);
+      this.chargingStation.getConnector(connectorId).status = OCPP16ChargePointStatus.AVAILABLE;
+    }
+    logger.warn(this.chargingStation.logPrefix() + ' Remote starting transaction REJECTED on connector Id ' + connectorId.toString() + ', idTag ' + idTag + ', availability ' + this.chargingStation.getConnector(connectorId).availability + ', status ' + this.chargingStation.getConnector(connectorId).status);
     return Constants.OCPP_RESPONSE_REJECTED;
   }
 
-  private async setRemoteStartChargingProfile(connectorId: number, cp: OCPP16ChargingProfile): Promise<boolean> {
+  private setRemoteStartTransactionChargingProfile(connectorId: number, cp: OCPP16ChargingProfile): boolean {
     if (cp && cp.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
       this.chargingStation.setChargingProfile(connectorId, cp);
       logger.debug(`${this.chargingStation.logPrefix()} Charging profile(s) set at remote start transaction, dump their stack: %j`, this.chargingStation.getConnector(connectorId).chargingProfiles);
       return true;
     } else if (cp && cp.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
-      await this.chargingStation.ocppRequestService.sendStatusNotification(connectorId, OCPP16ChargePointStatus.AVAILABLE);
-      this.chargingStation.getConnector(connectorId).status = OCPP16ChargePointStatus.AVAILABLE;
       logger.warn(`${this.chargingStation.logPrefix()} Not allowed to set ${cp.chargingProfilePurpose} charging profile(s) at remote start transaction`);
       return false;
+    } else if (!cp) {
+      return true;
     }
   }
 
@@ -332,4 +348,53 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
     logger.info(this.chargingStation.logPrefix() + ' Trying to remote stop a non existing transaction ' + transactionId.toString());
     return Constants.OCPP_RESPONSE_REJECTED;
   }
+
+  private async handleRequestGetDiagnostics(commandPayload: GetDiagnosticsRequest): Promise<GetDiagnosticsResponse> {
+    logger.debug(this.chargingStation.logPrefix() + ' ' + IncomingRequestCommand.GET_DIAGNOSTICS + ' request received: %j', commandPayload);
+    const uri = new url.URL(commandPayload.location);
+    if (uri.protocol.startsWith('ftp:')) {
+      let ftpClient: Client;
+      try {
+        const logFiles = fs.readdirSync(path.resolve(__dirname, '../../../../')).filter((file) => file.endsWith('.log')).map((file) => path.join('./', file));
+        const diagnosticsArchive = this.chargingStation.stationInfo.chargingStationId + '_logs.tar.gz';
+        tar.create({ gzip: true }, logFiles).pipe(fs.createWriteStream(diagnosticsArchive));
+        ftpClient = new Client();
+        const accessResponse = await ftpClient.access({
+          host: uri.host,
+          ...(uri.port !== '') && { port: Utils.convertToInt(uri.port) },
+          ...(uri.username !== '') && { user: uri.username },
+          ...(uri.password !== '') && { password: uri.password },
+        });
+        let uploadResponse: FTPResponse;
+        if (accessResponse.code === 220) {
+          // eslint-disable-next-line @typescript-eslint/no-misused-promises
+          ftpClient.trackProgress(async (info) => {
+            logger.info(`${this.chargingStation.logPrefix()} ${info.bytes / 1024} bytes transferred from diagnostics archive ${info.name}`);
+            await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(OCPP16DiagnosticsStatus.Uploading);
+          });
+          uploadResponse = await ftpClient.uploadFrom(path.join(path.resolve(__dirname, '../../../../'), diagnosticsArchive), uri.pathname + diagnosticsArchive);
+          if (uploadResponse.code === 226) {
+            await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(OCPP16DiagnosticsStatus.Uploaded);
+            if (ftpClient) {
+              ftpClient.close();
+            }
+            return { fileName: diagnosticsArchive };
+          }
+          throw new Error(`Diagnostics transfer failed with error code ${accessResponse.code.toString()}${uploadResponse?.code && '|' + uploadResponse?.code.toString()}`);
+        }
+        throw new Error(`Diagnostics transfer failed with error code ${accessResponse.code.toString()}${uploadResponse?.code && '|' + uploadResponse?.code.toString()}`);
+      } catch (error) {
+        await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(OCPP16DiagnosticsStatus.UploadFailed);
+        this.handleIncomingRequestError(IncomingRequestCommand.GET_DIAGNOSTICS, error);
+        if (ftpClient) {
+          ftpClient.close();
+        }
+        return Constants.OCPP_RESPONSE_EMPTY;
+      }
+    } else {
+      logger.error(`${this.chargingStation.logPrefix()} Unsupported protocol ${uri.protocol} to transfer the diagnostic logs archive`);
+      await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(OCPP16DiagnosticsStatus.UploadFailed);
+      return Constants.OCPP_RESPONSE_EMPTY;
+    }
+  }
 }