Merge branch 'main' into reservation-feature
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16IncomingRequestService.ts
index a33d0c26a60bb362f449c29d65b2bac57dd23e41..6917533e529e56d353a247bf4b538780256c68aa 100644 (file)
@@ -24,6 +24,7 @@ import {
   type ClearChargingProfileRequest,
   type ClearChargingProfileResponse,
   type ConnectorStatus,
+  ConnectorStatusEnum,
   ErrorType,
   type GenericResponse,
   GenericStatus,
@@ -35,8 +36,6 @@ import {
   type JsonObject,
   type JsonType,
   OCPP16AuthorizationStatus,
-  type OCPP16AuthorizeRequest,
-  type OCPP16AuthorizeResponse,
   OCPP16AvailabilityType,
   type OCPP16BootNotificationRequest,
   type OCPP16BootNotificationResponse,
@@ -84,7 +83,17 @@ import {
   type UnlockConnectorRequest,
   type UnlockConnectorResponse,
 } from '../../../types';
+import type {
+  OCPP16CancelReservationRequest,
+  OCPP16ReserveNowRequest,
+} from '../../../types/ocpp/1.6/Requests';
+import { ReservationTerminationReason } from '../../../types/ocpp/1.6/Reservation';
+import type {
+  OCPP16CancelReservationResponse,
+  OCPP16ReserveNowResponse,
+} from '../../../types/ocpp/1.6/Responses';
 import { Constants, Utils, logger } from '../../../utils';
+import { OCPPConstants } from '../OCPPConstants';
 import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService';
 
 const moduleName = 'OCPP16IncomingRequestService';
@@ -138,12 +147,17 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.handleRequestTriggerMessage.bind(this)],
       [OCPP16IncomingRequestCommand.DATA_TRANSFER, this.handleRequestDataTransfer.bind(this)],
       [OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, this.handleRequestUpdateFirmware.bind(this)],
+      [OCPP16IncomingRequestCommand.RESERVE_NOW, this.handleRequestReserveNow.bind(this)],
+      [
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+        this.handleRequestCancelReservation.bind(this),
+      ],
     ]);
     this.jsonSchemas = new Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>([
       [
         OCPP16IncomingRequestCommand.RESET,
         OCPP16ServiceUtils.parseJsonSchemaFile<ResetRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/Reset.json',
+          'assets/json-schemas/ocpp/1.6/Reset.json',
           moduleName,
           'constructor'
         ),
@@ -151,7 +165,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.CLEAR_CACHE,
         OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearCacheRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/ClearCache.json',
+          'assets/json-schemas/ocpp/1.6/ClearCache.json',
           moduleName,
           'constructor'
         ),
@@ -159,7 +173,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
         OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/UnlockConnector.json',
+          'assets/json-schemas/ocpp/1.6/UnlockConnector.json',
           moduleName,
           'constructor'
         ),
@@ -167,7 +181,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.GET_CONFIGURATION,
         OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/GetConfiguration.json',
+          'assets/json-schemas/ocpp/1.6/GetConfiguration.json',
           moduleName,
           'constructor'
         ),
@@ -175,7 +189,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
         OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/ChangeConfiguration.json',
+          'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json',
           moduleName,
           'constructor'
         ),
@@ -183,7 +197,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
         OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/GetDiagnostics.json',
+          'assets/json-schemas/ocpp/1.6/GetDiagnostics.json',
           moduleName,
           'constructor'
         ),
@@ -191,7 +205,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
         OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16GetCompositeScheduleRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/GetCompositeSchedule.json',
+          'assets/json-schemas/ocpp/1.6/GetCompositeSchedule.json',
           moduleName,
           'constructor'
         ),
@@ -199,7 +213,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
         OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/SetChargingProfile.json',
+          'assets/json-schemas/ocpp/1.6/SetChargingProfile.json',
           moduleName,
           'constructor'
         ),
@@ -207,7 +221,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
         OCPP16ServiceUtils.parseJsonSchemaFile<ClearChargingProfileRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/ClearChargingProfile.json',
+          'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json',
           moduleName,
           'constructor'
         ),
@@ -215,7 +229,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
         OCPP16ServiceUtils.parseJsonSchemaFile<ChangeAvailabilityRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/ChangeAvailability.json',
+          'assets/json-schemas/ocpp/1.6/ChangeAvailability.json',
           moduleName,
           'constructor'
         ),
@@ -223,7 +237,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
         OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStartTransactionRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json',
+          'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json',
           moduleName,
           'constructor'
         ),
@@ -231,7 +245,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
         OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStopTransactionRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json',
+          'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json',
           moduleName,
           'constructor'
         ),
@@ -239,7 +253,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
         OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/TriggerMessage.json',
+          'assets/json-schemas/ocpp/1.6/TriggerMessage.json',
           moduleName,
           'constructor'
         ),
@@ -247,7 +261,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.DATA_TRANSFER,
         OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/DataTransfer.json',
+          'assets/json-schemas/ocpp/1.6/DataTransfer.json',
           moduleName,
           'constructor'
         ),
@@ -255,7 +269,23 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       [
         OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
         OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareRequest>(
-          '../../../assets/json-schemas/ocpp/1.6/UpdateFirmware.json',
+          'assets/json-schemas/ocpp/1.6/UpdateFirmware.json',
+          moduleName,
+          'constructor'
+        ),
+      ],
+      [
+        OCPP16IncomingRequestCommand.RESERVE_NOW,
+        OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowRequest>(
+          'assets/json-schemas/ocpp/1.6/ReserveNow.json',
+          moduleName,
+          'constructor'
+        ),
+      ],
+      [
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+        OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationRequest>(
+          'assets/json-schemas/ocpp/1.6/CancelReservation.json',
           moduleName,
           'constructor'
         ),
@@ -799,6 +829,18 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     commandPayload: RemoteStartTransactionRequest
   ): Promise<GenericResponse> {
     const transactionConnectorId = commandPayload.connectorId;
+    const reserved =
+      chargingStation.getConnectorStatus(transactionConnectorId).status ===
+      OCPP16ChargePointStatus.Reserved;
+    if (
+      reserved &&
+      chargingStation.validateIncomingRequestWithReservation(
+        transactionConnectorId,
+        commandPayload.idTag
+      )
+    ) {
+      return OCPP16Constants.OCPP_RESPONSE_REJECTED;
+    }
     if (chargingStation.hasConnector(transactionConnectorId) === false) {
       return this.notifyRemoteStartTransactionRejected(
         chargingStation,
@@ -807,8 +849,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       );
     }
     if (
-      chargingStation.isChargingStationAvailable() === false ||
-      chargingStation.isConnectorAvailable(transactionConnectorId) === false
+      !chargingStation.isChargingStationAvailable() ||
+      !chargingStation.isConnectorAvailable(transactionConnectorId)
     ) {
       return this.notifyRemoteStartTransactionRejected(
         chargingStation,
@@ -827,36 +869,10 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     const connectorStatus = chargingStation.getConnectorStatus(transactionConnectorId);
     // Check if authorized
     if (chargingStation.getAuthorizeRemoteTxRequests() === true) {
-      let authorized = false;
-      if (
-        chargingStation.getLocalAuthListEnabled() === true &&
-        chargingStation.hasIdTags() === true &&
-        Utils.isNotEmptyString(
-          chargingStation.idTagsCache
-            .getIdTags(ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo))
-            ?.find((idTag) => idTag === commandPayload.idTag)
-        )
-      ) {
-        connectorStatus.localAuthorizeIdTag = commandPayload.idTag;
-        connectorStatus.idTagLocalAuthorized = true;
-        authorized = true;
-      } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) {
-        connectorStatus.authorizeIdTag = commandPayload.idTag;
-        const authorizeResponse: OCPP16AuthorizeResponse =
-          await chargingStation.ocppRequestService.requestHandler<
-            OCPP16AuthorizeRequest,
-            OCPP16AuthorizeResponse
-          >(chargingStation, OCPP16RequestCommand.AUTHORIZE, {
-            idTag: commandPayload.idTag,
-          });
-        if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
-          authorized = true;
-        }
-      } else {
-        logger.warn(
-          `${chargingStation.logPrefix()} The charging station configuration expects authorize at remote start transaction but local authorization or authorize isn't enabled`
-        );
-      }
+      const authorized = await chargingStation.isAuthorized(
+        transactionConnectorId,
+        commandPayload.idTag
+      );
       if (authorized === true) {
         // Authorization successful, start transaction
         if (
@@ -867,15 +883,24 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
           ) === true
         ) {
           connectorStatus.transactionRemoteStarted = true;
+          const startTransactionPayload: JsonType = {
+            connectorId: transactionConnectorId,
+            idTag: commandPayload.idTag,
+          };
+          if (reserved) {
+            const reservation = chargingStation.getReservationByConnectorId(transactionConnectorId);
+            startTransactionData.reservationId = reservation.id;
+            await chargingStation.removeReservation(
+              reservation,
+              ReservationTerminationReason.TRANSACTION_STARTED
+            );
+          }
           if (
             (
               await chargingStation.ocppRequestService.requestHandler<
                 OCPP16StartTransactionRequest,
                 OCPP16StartTransactionResponse
-              >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
-                connectorId: transactionConnectorId,
-                idTag: commandPayload.idTag,
-              })
+              >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, startTransactionData)
             ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
           ) {
             logger.debug(remoteStartTransactionLogMsg);
@@ -1244,7 +1269,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       let ftpClient: Client;
       try {
         const logFiles = fs
-          .readdirSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../'))
+          .readdirSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'))
           .filter((file) => file.endsWith('.log'))
           .map((file) => path.join('./', file));
         const diagnosticsArchive = `${chargingStation.stationInfo.chargingStationId}_logs.tar.gz`;
@@ -1282,7 +1307,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
           });
           uploadResponse = await ftpClient.uploadFrom(
             path.join(
-              path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../'),
+              path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
               diagnosticsArchive
             ),
             `${uri.pathname}${diagnosticsArchive}`
@@ -1504,4 +1529,88 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       );
     }
   }
+
+  private async handleRequestReserveNow(
+    chargingStation: ChargingStation,
+    commandPayload: OCPP16ReserveNowRequest
+  ): Promise<OCPP16ReserveNowResponse> {
+    const { reservationId, idTag, connectorId } = commandPayload;
+    let response: OCPP16ReserveNowResponse;
+    try {
+      if (
+        !chargingStation.supportsReservations() &&
+        chargingStation.isConnectorAvailable(connectorId)
+      ) {
+        return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED;
+      }
+      if (connectorId === 0 && !chargingStation.supportsReservationsOnConnectorId0()) {
+        return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED;
+      }
+      if (!(await chargingStation.isAuthorized(connectorId, idTag))) {
+        return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED;
+      }
+      switch (chargingStation.getConnectorStatus(connectorId).status) {
+        case ConnectorStatusEnum.Faulted:
+          response = OCPPConstants.OCPP_RESERVATION_RESPONSE_FAULTED;
+          break;
+        case ConnectorStatusEnum.Occupied:
+          response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED;
+          break;
+        case ConnectorStatusEnum.Unavailable:
+          response = OCPPConstants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE;
+          break;
+        case ConnectorStatusEnum.Reserved:
+          if (!chargingStation.isConnectorReservable(reservationId, connectorId, idTag)) {
+            response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED;
+            break;
+          }
+        // eslint-disable-next-line no-fallthrough
+        default:
+          if (!chargingStation.isConnectorReservable(reservationId)) {
+            response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED;
+            break;
+          }
+          await chargingStation.addReservation({
+            id: commandPayload.reservationId,
+            ...commandPayload,
+          });
+          response = OCPPConstants.OCPP_RESERVATION_RESPONSE_ACCEPTED;
+          break;
+      }
+      return response;
+    } catch (error) {
+      chargingStation.getConnectorStatus(connectorId).status = ConnectorStatusEnum.Available;
+      return this.handleIncomingRequestError(
+        chargingStation,
+        OCPP16IncomingRequestCommand.RESERVE_NOW,
+        error as Error,
+        { errorResponse: OCPPConstants.OCPP_RESERVATION_RESPONSE_FAULTED }
+      );
+    }
+  }
+
+  private async handleRequestCancelReservation(
+    chargingStation: ChargingStation,
+    commandPayload: OCPP16CancelReservationRequest
+  ): Promise<OCPP16CancelReservationResponse> {
+    try {
+      const { reservationId } = commandPayload;
+      const [exists, reservation] = chargingStation.doesReservationExists({ id: reservationId });
+      if (!exists) {
+        logger.error(
+          `${chargingStation.logPrefix()} Reservation with ID ${reservationId} does not exist on charging station`
+        );
+        return OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED;
+      }
+      await chargingStation.removeReservation(reservation);
+      return OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED;
+    } catch (error) {
+      return this.handleIncomingRequestError(
+        chargingStation,
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+        error as Error,
+        { errorResponse: OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED }
+      );
+    }
+  }
 }