build(simulator): features for reserve-now and cancel-reservation support added
authorJulian Buecher <julian.buecher@gmx.de>
Wed, 24 May 2023 21:12:18 +0000 (23:12 +0200)
committerJulian Buecher <julian.buecher@gmx.de>
Wed, 24 May 2023 21:12:18 +0000 (23:12 +0200)
18 files changed:
README.md
src/assets/station-templates/abb-atg.station-template.json
src/assets/station-templates/abb.station-template.json
src/assets/station-templates/evlink.station-template.json
src/assets/station-templates/keba.station-template.json
src/assets/station-templates/schneider-imredd.station-template.json
src/assets/station-templates/schneider.station-template.json
src/assets/station-templates/siemens.station-template.json
src/assets/station-templates/virtual-simple-atg.station-template.json
src/assets/station-templates/virtual-simple.station-template.json
src/assets/station-templates/virtual.station-template.json
src/charging-station/ChargingStation.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/types/ChargingStationTemplate.ts
src/types/ocpp/1.6/Requests.ts
src/types/ocpp/1.6/Reservation.ts [new file with mode: 0644]
src/types/ocpp/1.6/Responses.ts
src/types/ocpp/Reservation.ts [new file with mode: 0644]

index a3f0e4e76a568e1cc063a1ea8096baab535b7568..9fb599ae77d2589ba89b5a3539e8b0977a6238da 100644 (file)
--- a/README.md
+++ b/README.md
@@ -389,8 +389,8 @@ make SUBMODULES_INIT=true
 
 #### Reservation Profile
 
-- :x: CancelReservation
-- :x: ReserveNow
+- :white_check_mark: CancelReservation
+- :white_check_mark: ReserveNow
 
 #### Smart Charging Profile
 
index 9414a19e7bbdfd958049fe517bcef77b92b2e852..99a67c648f0c671fbbe8879b2d4e9ed378c26200 100644 (file)
@@ -29,7 +29,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "true"
       }
     ]
   },
index 0c6b233adf47329098666c927ef500ccbdbcfa38..a95517e7d67a12b65bfb4fb479a324377eea327d 100644 (file)
@@ -29,7 +29,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "false"
       }
     ]
   },
index fda88bfb9bfa54b72cafc6d7a088f0d7d3e48cd5..05a8b3da3bcbdda1898c82c21dff57c48c070401 100644 (file)
@@ -30,7 +30,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
       {
         "key": "AuthorizeRemoteTxRequests",
         "readonly": false,
-        "value": "false"
+        "value": "true"
       },
       {
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "true"
       }
     ]
   },
index 0d9583658498a668ef31d9c020492e5a0d8797d3..6c0e3437269f4fd3790c4d5d9ddd50a0a7b73487 100644 (file)
@@ -27,7 +27,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "false"
       }
     ]
   },
index 0369d0ac428cb342e9606cb9695f8a50af519367..d6cc99f8a82d1f095b6f6427ce1dde3947233532 100644 (file)
@@ -29,7 +29,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "true"
       }
     ]
   },
index 4dd714731bbd4a0fa6bb5e1168f4860c71146750..8fb25abec7f96d5b2e7814f5a9c4690d2a0e475c 100644 (file)
@@ -29,7 +29,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "false"
       }
     ]
   },
index aca529ace209bf94da67a4a114bd64242f6cd322..ac075ce6f58a77a9fdf5998b6f7dee8d29b2304b 100644 (file)
@@ -24,7 +24,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,LocalAuthListManagement"
+        "value": "Core,LocalAuthListManagement,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "true"
       }
     ]
   },
index 0dbd6782798ae7c67c0a0ec1a765c07928344e83..aedd27cbbeda051d9ee1d86708ac62d36f75c819 100644 (file)
@@ -24,7 +24,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "false"
       }
     ]
   },
index d94b5d39991b04e2cb08dd89c1837bd682443d0c..8a0c28011a8f9e0a7bef52754b0bc6843b15aa5f 100644 (file)
@@ -24,7 +24,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "true"
       }
     ]
   },
index 3980cba66a60734834cbb0781962eb5c46f524c0..962e792fe3ae444f3fc1188cd2cca7e04c1262cf 100644 (file)
@@ -24,7 +24,7 @@
       {
         "key": "SupportedFeatureProfiles",
         "readonly": true,
-        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger"
+        "value": "Core,FirmwareManagement,LocalAuthListManagement,SmartCharging,RemoteTrigger,Reservation"
       },
       {
         "key": "LocalAuthListEnabled",
index f47952ca9cf4b326c4a98121aec3f8d044f6ca82..ace7aff1cfb67c29f6c291cdee78bbc7b659a977 100644 (file)
@@ -26,6 +26,7 @@ import {
   type OCPPRequestService,
   OCPPServiceUtils,
 } from './ocpp';
+import { OCPPConstants } from './ocpp/OCPPConstants';
 import { SharedLRUCache } from './SharedLRUCache';
 import { BaseError, OCPPError } from '../exception';
 import { PerformanceStatistics } from '../performance';
@@ -62,6 +63,8 @@ import {
   MeterValueMeasurand,
   type MeterValuesRequest,
   type MeterValuesResponse,
+  OCPP16SupportedFeatureProfiles,
+  OCPP20ConnectorStatusEnumType,
   OCPPVersion,
   type OutgoingRequest,
   PowerUnits,
@@ -81,6 +84,7 @@ import {
   WebSocketCloseEventStatusCode,
   type WsOptions,
 } from '../types';
+import type { Reservation } from '../types/ocpp/Reservation';
 import {
   ACElectricUtils,
   AsyncLock,
@@ -132,6 +136,7 @@ export class ChargingStation {
   private readonly sharedLRUCache: SharedLRUCache;
   private webSocketPingSetInterval!: NodeJS.Timeout;
   private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
+  private reservations?: Reservation[];
 
   constructor(index: number, templateFile: string) {
     this.started = false;
@@ -888,6 +893,80 @@ export class ChargingStation {
     );
   }
 
+  public supportsReservations(): boolean {
+    logger.info(`${this.logPrefix()} Check for reservation support in charging station`);
+    return ChargingStationConfigurationUtils.getConfigurationKey(
+      this,
+      StandardParametersKey.SupportedFeatureProfiles
+    ).value.includes(OCPP16SupportedFeatureProfiles.Reservation);
+  }
+
+  public supportsReservationsOnConnectorId0(): boolean {
+    logger.info(
+      `Check for reservation support on connector 0 in charging station (CS): ${this.logPrefix()}`
+    );
+    return (
+      this.supportsReservations() &&
+      ChargingStationConfigurationUtils.getConfigurationKey(
+        this,
+        OCPPConstants.OCPP_RESERVE_CONNECTOR_ZERO_SUPPORTED
+      ).value === 'true'
+    );
+  }
+
+  public addReservation(newReservation: Reservation): void {
+    if (Utils.isNullOrUndefined(this.reservations)) {
+      this.reservations = [];
+    }
+    const [exists, foundReservation] = this.doesReservationExist(newReservation.reservationId);
+    if (exists) {
+      this.replaceExistingReservation(foundReservation, newReservation);
+    } else {
+      this.reservations.push(newReservation);
+    }
+  }
+
+  public removeReservation(existingReservationId: number): void {
+    const index = this.reservations.findIndex((res) => res.reservationId === existingReservationId);
+    this.reservations.splice(index, 1);
+  }
+
+  public getReservation(reservationId: number, reservationIndex?: number): Reservation {
+    if (!Utils.isNullOrUndefined(reservationIndex)) {
+      return this.reservations[reservationIndex];
+    }
+    return this.reservations.find((r) => r.reservationId === reservationId);
+  }
+
+  public doesReservationExist(
+    reservationId: number,
+    reservation?: Reservation
+  ): [boolean, Reservation] {
+    const foundReservation = this.reservations.find(
+      (r) => r.reservationId === reservationId || r.reservationId === reservation.reservationId
+    );
+    return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation];
+  }
+
+  public getReservationByConnectorId(connectorId: number): Reservation {
+    return this.reservations.find((r) => r.connectorId === connectorId);
+  }
+
+  public getAvailableConnector(): Map<number, ConnectorStatus> {
+    for (const connectorId in this.connectors) {
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+      const connector = this.connectors[Utils.convertToInt(connectorId)];
+      if (
+        this.isConnectorAvailable(Utils.convertToInt(connectorId)) &&
+        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+        connector.status === OCPP20ConnectorStatusEnumType.Available
+      ) {
+        // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+        return connector;
+      }
+    }
+  }
+
   private flushMessageBuffer(): void {
     if (this.messageBuffer.size > 0) {
       for (const message of this.messageBuffer.values()) {
@@ -915,6 +994,16 @@ export class ChargingStation {
     return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
   }
 
+  private replaceExistingReservation(
+    existingReservation: Reservation,
+    newReservation: Reservation
+  ): void {
+    const existingReservationIndex = this.reservations.findIndex(
+      (r) => r.reservationId === existingReservation.reservationId
+    );
+    this.reservations.splice(existingReservationIndex, 1, newReservation);
+  }
+
   private getSupervisionUrlOcppKey(): string {
     return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
   }
index 9e4ff3e0d9f19b839d5f17d91b92d02a43407838..46c1201872280760d6972836d4f8c1e80ecf6695 100644 (file)
@@ -1,3 +1,4 @@
+/* eslint-disable max-len */
 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
 import fs from 'node:fs';
@@ -24,6 +25,7 @@ import {
   type ClearChargingProfileRequest,
   type ClearChargingProfileResponse,
   type ConnectorStatus,
+  ConnectorStatusEnum,
   ErrorType,
   type GenericResponse,
   GenericStatus,
@@ -84,7 +86,16 @@ import {
   type UnlockConnectorRequest,
   type UnlockConnectorResponse,
 } from '../../../types';
+import type {
+  OCPP16CancelReservationRequest,
+  OCPP16ReserveNowRequest,
+} from '../../../types/ocpp/1.6/Requests';
+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';
@@ -799,6 +810,9 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     commandPayload: RemoteStartTransactionRequest
   ): Promise<GenericResponse> {
     const transactionConnectorId = commandPayload.connectorId;
+    const reserved: boolean =
+      chargingStation.getConnectorStatus(transactionConnectorId).status ===
+      OCPP16ChargePointStatus.Reserved;
     if (chargingStation.hasConnector(transactionConnectorId) === false) {
       return this.notifyRemoteStartTransactionRejected(
         chargingStation,
@@ -808,7 +822,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     }
     if (
       chargingStation.isChargingStationAvailable() === false ||
-      chargingStation.isConnectorAvailable(transactionConnectorId) === false
+      chargingStation.isConnectorAvailable(transactionConnectorId) === false ||
+      reserved
     ) {
       return this.notifyRemoteStartTransactionRejected(
         chargingStation,
@@ -827,36 +842,11 @@ 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 this.isAuthorized(
+        chargingStation,
+        transactionConnectorId,
+        commandPayload.idTag
+      );
       if (authorized === true) {
         // Authorization successful, start transaction
         if (
@@ -867,15 +857,20 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
           ) === true
         ) {
           connectorStatus.transactionRemoteStarted = true;
+          const startTransactionData: JsonType = {
+            connectorId: transactionConnectorId,
+            idTag: commandPayload.idTag,
+          };
+          if (reserved) {
+            startTransactionData['reservationId'] =
+              chargingStation.getReservationByConnectorId(transactionConnectorId).reservationId;
+          }
           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);
@@ -899,6 +894,13 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         commandPayload.idTag
       );
     }
+    if (reserved) {
+      await this.handleReservedRemoteStartTransaction(
+        chargingStation,
+        transactionConnectorId,
+        commandPayload
+      );
+    }
     // No authorization check required, start transaction
     if (
       this.setRemoteStartTransactionChargingProfile(
@@ -1504,4 +1506,211 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       );
     }
   }
+
+  private async handleRequestReserveNow(
+    chargingStation: ChargingStation,
+    commandPayload: OCPP16ReserveNowRequest
+  ): Promise<OCPP16ReserveNowResponse> {
+    const { reservationId, idTag, connectorId } = commandPayload;
+    let connector: Map<number, ConnectorStatus>;
+    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 this.isAuthorized(chargingStation, connectorId, commandPayload.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 (Utils.isUndefined(chargingStation.getReservation(commandPayload.reservationId))) {
+            response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED;
+            break;
+          }
+        // eslint-disable-next-line no-fallthrough
+        default:
+          logger.info(
+            `${chargingStation.logPrefix()} on connector ${connectorId} is now reserved for ${
+              commandPayload.idTag
+            }`
+          );
+          chargingStation.getConnectorStatus(connectorId).status = ConnectorStatusEnum.Reserved;
+          chargingStation.addReservation({ ...commandPayload });
+          await chargingStation.ocppRequestService
+            .requestHandler<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
+              chargingStation,
+              OCPP16RequestCommand.STATUS_NOTIFICATION,
+              {
+                connectorId,
+                errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
+                status: chargingStation.getConnectorStatus(connectorId).status,
+              },
+              {
+                triggerMessage: true,
+              }
+            )
+            .catch(Constants.EMPTY_FUNCTION);
+          response = OCPPConstants.OCPP_RESERVATION_RESPONSE_ACCEPTED;
+          break;
+      }
+      return response;
+    } catch (error) {
+      return this.handleIncomingRequestError(
+        chargingStation,
+        OCPP16IncomingRequestCommand.RESERVE_NOW,
+        error as Error,
+        { errorResponse: OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED }
+      );
+    }
+  }
+
+  private async handleRequestCancelReservation(
+    chargingStation: ChargingStation,
+    commandPayload: OCPP16CancelReservationRequest
+  ): Promise<OCPP16CancelReservationResponse> {
+    try {
+      const reservationId = commandPayload.reservationId;
+      const [exists, reservation] = chargingStation.doesReservationExist(reservationId);
+      if (!exists) {
+        logger.error(
+          `${chargingStation.logPrefix()} Reservation with ID ${reservationId} does not exist on charging station`
+        );
+        return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED;
+      }
+      chargingStation.getConnectorStatus(reservation.connectorId).status =
+        ConnectorStatusEnum.Available;
+      chargingStation.removeReservation(reservation.reservationId);
+      await chargingStation.ocppRequestService
+        .requestHandler<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
+          chargingStation,
+          OCPP16RequestCommand.STATUS_NOTIFICATION,
+          {
+            connectorId: reservation.connectorId,
+            errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
+            status: chargingStation.getConnectorStatus(reservation.connectorId).status,
+          },
+          {
+            triggerMessage: true,
+          }
+        )
+        .catch(Constants.EMPTY_FUNCTION);
+      return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED;
+    } catch (error) {
+      return this.handleIncomingRequestError(
+        chargingStation,
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+        error as Error,
+        { errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED }
+      );
+    }
+  }
+
+  /**
+   * Check for authorized access on a connector with given ConnectorId and idTag for the user
+   * @param {ChargingStation} chargingStation - Charging Station working on incoming request
+   * @param {number} ConnectorId - Identifier of the connector at the charging station
+   * @param {string} idTag - Identifier of the user
+   * @param {string} parentIdTag - Identifier for a group of idTags, which is optional
+   * @returns {Promise<boolean>} - 'true' if user is authorized, 'false' otherwise
+   */
+  private async isAuthorized(
+    chargingStation: ChargingStation,
+    connectorId: number,
+    idTag: string,
+    parentIdTag?: string
+  ): Promise<boolean> {
+    let authorized = false;
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+    if (
+      chargingStation.getLocalAuthListEnabled() === true &&
+      chargingStation.hasIdTags() === true &&
+      Utils.isNotEmptyString(
+        chargingStation.idTagsCache
+          .getIdTags(ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo))
+          ?.find((tag) => tag === idTag)
+      )
+    ) {
+      connectorStatus.localAuthorizeIdTag = idTag;
+      connectorStatus.idTagLocalAuthorized = true;
+      authorized = true;
+    } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) {
+      connectorStatus.authorizeIdTag = idTag;
+      const authorizeResponse: OCPP16AuthorizeResponse =
+        await chargingStation.ocppRequestService.requestHandler<
+          OCPP16AuthorizeRequest,
+          OCPP16AuthorizeResponse
+        >(chargingStation, OCPP16RequestCommand.AUTHORIZE, {
+          idTag: 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`
+      );
+    }
+    return authorized;
+  }
+
+  private async handleReservedRemoteStartTransaction(
+    chargingStation: ChargingStation,
+    connectorId: number,
+    commandPayload: RemoteStartTransactionRequest
+  ): Promise<GenericResponse> {
+    const reservation = chargingStation.getReservationByConnectorId(connectorId);
+    if (
+      !Utils.isUndefined(reservation) &&
+      (await this.isAuthorized(chargingStation, connectorId, commandPayload.idTag)) &&
+      reservation.idTag === commandPayload.idTag
+    ) {
+      const remoteStartTransactionLogMsg = `${chargingStation.logPrefix()} Transaction remotely STARTED on ${
+        chargingStation.stationInfo.chargingStationId
+      }#${connectorId.toString()} for idTag '${commandPayload.idTag}'`;
+      await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+        chargingStation,
+        connectorId,
+        OCPP16ChargePointStatus.Preparing
+      );
+      if (
+        this.setRemoteStartTransactionChargingProfile(
+          chargingStation,
+          connectorId,
+          commandPayload.chargingProfile
+        ) === true
+      ) {
+        chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted = true;
+        if (
+          (
+            await chargingStation.ocppRequestService.requestHandler<
+              OCPP16StartTransactionRequest,
+              OCPP16StartTransactionResponse
+            >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
+              connectorId: connectorId,
+              idTag: commandPayload.idTag,
+              reservationId: reservation.reservationId,
+            })
+          ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
+        ) {
+          logger.debug(remoteStartTransactionLogMsg);
+          return OCPP16Constants.OCPP_RESPONSE_ACCEPTED;
+        }
+      }
+    }
+  }
 }
index e3594d02200701ce1502659300e265a8d07278fc..a11ac892779ff535f334f8052a671aa4d17243e1 100644 (file)
@@ -14,6 +14,7 @@ import type {
   MessageTrigger,
   RequestCommand,
 } from './ocpp/Requests';
+import type { Reservation } from './ocpp/Reservation';
 
 export enum CurrentType {
   AC = 'AC',
@@ -115,4 +116,5 @@ export type ChargingStationTemplate = {
   AutomaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration;
   Evses?: Record<string, EvseTemplate>;
   Connectors?: Record<string, ConnectorStatus>;
+  reservation?: Reservation[];
 };
index 786ea617be0fb38e518003d52ffcacd007ca82af..a04a3f65ba61f5321e16e2936f54729bd018bcc8 100644 (file)
@@ -21,6 +21,8 @@ export enum OCPP16RequestCommand {
   DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification',
   FIRMWARE_STATUS_NOTIFICATION = 'FirmwareStatusNotification',
   DATA_TRANSFER = 'DataTransfer',
+  RESERVE_NOW = 'ReserveNow',
+  CANCEL_RESERVATION = 'CancelReservation',
 }
 
 export enum OCPP16IncomingRequestCommand {
@@ -39,6 +41,8 @@ export enum OCPP16IncomingRequestCommand {
   TRIGGER_MESSAGE = 'TriggerMessage',
   DATA_TRANSFER = 'DataTransfer',
   UPDATE_FIRMWARE = 'UpdateFirmware',
+  RESERVE_NOW = 'ReserveNow',
+  CANCEL_RESERVATION = 'CancelReservation',
 }
 
 export type OCPP16HeartbeatRequest = EmptyObject;
@@ -185,7 +189,7 @@ export interface OCPP16DataTransferRequest extends JsonObject {
 }
 
 export interface OCPP16ReserveNowRequest {
-  connectorId: string;
+  connectorId: number;
   expiryDate: Date;
   idTag: string;
   parentIdTag?: string;
diff --git a/src/types/ocpp/1.6/Reservation.ts b/src/types/ocpp/1.6/Reservation.ts
new file mode 100644 (file)
index 0000000..7a78225
--- /dev/null
@@ -0,0 +1,7 @@
+export interface OCPP16Reservation {
+  connectorId: number;
+  expiryDate: Date;
+  idTag: string;
+  parentIdTag?: string;
+  reservationId: number;
+}
index 6a5ebfe4cd7b4ae58762f67b6697d748dd1ceb58..15c76a71ee87d9206ec28c46d2720f9076c39be9 100644 (file)
@@ -110,23 +110,23 @@ export interface OCPP16DataTransferResponse extends JsonObject {
   data?: string;
 }
 
-export enum OCPP16ReservationStatus {
+export enum OCPP16CancelReservationStatus {
   ACCEPTED = 'Accepted',
-  FAULTED = 'Faulted',
-  OCCUPIED = 'Occupied',
   REJECTED = 'Rejected',
-  UNAVAILABLE = 'Unavailable',
 }
 
-export interface OCPP16ReserveNowResponse {
-  status: OCPP16ReservationStatus;
+export interface OCPP16CancelReservationResponse {
+  status: OCPP16CancelReservationStatus;
 }
 
-export enum OCPP16CancelReservationStatus {
+export enum OCPP16ReservationStatus {
   ACCEPTED = 'Accepted',
+  FAULTED = 'Faulted',
+  OCCUPIED = 'Occupied',
   REJECTED = 'Rejected',
+  UNAVAILABLE = 'Unavailable',
 }
 
-export interface OCPP16CancelReservationResponse {
-  status: OCPP16CancelReservationStatus;
+export interface OCPP16ReserveNowResponse {
+  status: OCPP16ReservationStatus;
 }
diff --git a/src/types/ocpp/Reservation.ts b/src/types/ocpp/Reservation.ts
new file mode 100644 (file)
index 0000000..dd5d819
--- /dev/null
@@ -0,0 +1,3 @@
+import { OCPP16Reservation } from './1.6/Reservation';
+
+export type Reservation = OCPP16Reservation;