Merge branch 'main' into reservation-feature
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 5 Jun 2023 15:36:45 +0000 (17:36 +0200)
committerGitHub <noreply@github.com>
Mon, 5 Jun 2023 15:36:45 +0000 (17:36 +0200)
28 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/chargex.station-template.json
src/assets/station-templates/evlink.station-template.json
src/assets/station-templates/keba.station-template.json
src/assets/station-templates/schneider-evses.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/charging-station/ocpp/1.6/OCPP16RequestService.ts
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/OCPPConstants.ts
src/types/ConnectorStatus.ts
src/types/index.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/Requests.ts
src/types/ocpp/Reservation.ts [new file with mode: 0644]
src/types/ocpp/Responses.ts
src/utils/Constants.ts

index e3a202ea7226121850ab5060f091b162188ca3bf..04771de2ff6647df14b63ec162a88ec675766dba 100644 (file)
--- a/README.md
+++ b/README.md
@@ -395,8 +395,8 @@ make SUBMODULES_INIT=true
 
 #### Reservation Profile
 
-- :x: CancelReservation
-- :x: ReserveNow
+- :white_check_mark: CancelReservation
+- :white_check_mark: ReserveNow
 
 #### Smart Charging Profile
 
@@ -466,7 +466,7 @@ All kind of OCPP parameters are supported in charging station configuration or c
 
 #### Reservation Profile
 
-- _none_
+- :white_check_mark: ReserveConnectorZeroSupported (type: boolean) (units: -)
 
 #### Smart Charging Profile
 
index 9414a19e7bbdfd958049fe517bcef77b92b2e852..456089bfd0ef584960b086c8bb3d0da13cb6f7fe 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 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 6ff7562d823567d5b93db2449df6e7c36082a969..fca570940e21de8fa7206c78592e3d930cd9ffbf 100644 (file)
       {
         "key": "TransactionMessageRetryInterval",
         "value": "20"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "false"
       }
     ]
   },
index fda88bfb9bfa54b72cafc6d7a088f0d7d3e48cd5..5b9f9eacfe813d6a672c96cce6a092aa60c436da 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": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "false"
       }
     ]
   },
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 7017f1e427f1382bf0cebee8ce4c87cc1175a364..f292519e1bed4eb5d0f3963b0c363758f91c78f3 100644 (file)
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
+      },
+      {
+        "key": "ReserveConnectorZeroSupported",
+        "readonly": false,
+        "value": "false"
       }
     ]
   },
index 0369d0ac428cb342e9606cb9695f8a50af519367..a4411047355b4b2da68f324b65dc679ad02d7b0a 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 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..439b5f92c53c2f67000d8c7a1c0bdbc6245bfeab 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": "false"
       }
     ]
   },
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..37cbe8ad4236a868d18ff1c42e911cfb7545d265 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 3980cba66a60734834cbb0781962eb5c46f524c0..09086f8e4901a9015ec9e4d4c58883cdfdeca0d2 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 9a8f468b37bee455378b890a2392a4e02a9dfefb..a3f6769607c5b01bb9774bf614a71f31a80a5afe 100644 (file)
@@ -67,6 +67,9 @@ import {
   PowerUnits,
   RegistrationStatusEnumType,
   RequestCommand,
+  type Reservation,
+  ReservationFilterKey,
+  ReservationTerminationReason,
   type Response,
   StandardParametersKey,
   type Status,
@@ -135,6 +138,7 @@ export class ChargingStation {
   private readonly sharedLRUCache: SharedLRUCache;
   private webSocketPingSetInterval!: NodeJS.Timeout;
   private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
+  private reservationExpiryDateSetInterval?: NodeJS.Timeout;
 
   constructor(index: number, templateFile: string) {
     this.started = false;
@@ -551,7 +555,8 @@ export class ChargingStation {
       );
     } else {
       logger.error(
-        `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`
+        `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()},
+          not starting the heartbeat`
       );
     }
   }
@@ -579,13 +584,15 @@ export class ChargingStation {
     }
     if (!this.getConnectorStatus(connectorId)) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on non existing connector id ${connectorId.toString()}`
+        `${this.logPrefix()} Trying to start MeterValues on non existing connector id
+          ${connectorId.toString()}`
       );
       return;
     }
     if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`
+        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
+          with no transaction started`
       );
       return;
     } else if (
@@ -593,7 +600,8 @@ export class ChargingStation {
       Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
     ) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`
+        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
+          with no transaction id`
       );
       return;
     }
@@ -645,6 +653,9 @@ export class ChargingStation {
         if (this.getEnableStatistics() === true) {
           this.performanceStatistics?.start();
         }
+        if (this.hasFeatureProfile(SupportedFeatureProfiles.Reservation)) {
+          this.startReservationExpiryDateSetInterval();
+        }
         this.openWSConnection();
         // Monitor charging station template file
         this.templateFileWatcher = watchJsonFile(
@@ -754,7 +765,8 @@ export class ChargingStation {
     params = { ...{ closeOpened: false, terminateOpened: false }, ...params };
     if (this.started === false && this.starting === false) {
       logger.warn(
-        `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station`
+        `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()}
+          on stopped charging station`
       );
       return;
     }
@@ -773,7 +785,8 @@ export class ChargingStation {
 
     if (this.isWebSocketConnectionOpened() === true) {
       logger.warn(
-        `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
+        `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()}
+          is already opened`
       );
       return;
     }
@@ -908,6 +921,199 @@ export class ChargingStation {
     );
   }
 
+  public getReservationOnConnectorId0Enabled(): boolean {
+    return Utils.convertToBoolean(
+      ChargingStationConfigurationUtils.getConfigurationKey(
+        this,
+        StandardParametersKey.ReserveConnectorZeroSupported
+      ).value
+    );
+  }
+
+  public async addReservation(reservation: Reservation): Promise<void> {
+    const [exists, reservationFound] = this.doesReservationExists(reservation);
+    if (exists) {
+      await this.removeReservation(reservationFound);
+    }
+    const connectorStatus = this.getConnectorStatus(reservation.connectorId);
+    connectorStatus.reservation = reservation;
+    connectorStatus.status = ConnectorStatusEnum.Reserved;
+    if (reservation.connectorId === 0) {
+      return;
+    }
+    await this.ocppRequestService.requestHandler<
+      StatusNotificationRequest,
+      StatusNotificationResponse
+    >(
+      this,
+      RequestCommand.STATUS_NOTIFICATION,
+      OCPPServiceUtils.buildStatusNotificationRequest(
+        this,
+        reservation.connectorId,
+        ConnectorStatusEnum.Reserved
+      )
+    );
+  }
+
+  public async removeReservation(
+    reservation: Reservation,
+    reason?: ReservationTerminationReason
+  ): Promise<void> {
+    const connector = this.getConnectorStatus(reservation.connectorId);
+    switch (reason) {
+      case ReservationTerminationReason.TRANSACTION_STARTED: {
+        delete connector.reservation;
+        if (reservation.connectorId === 0) {
+          connector.status = ConnectorStatusEnum.Available;
+        }
+        break;
+      }
+      case ReservationTerminationReason.CONNECTOR_STATE_CHANGED: {
+        delete connector.reservation;
+        break;
+      }
+      default: {
+        // ReservationTerminationReason.EXPIRED, ReservationTerminationReason.CANCELED
+        connector.status = ConnectorStatusEnum.Available;
+        delete connector.reservation;
+        await this.ocppRequestService.requestHandler<
+          StatusNotificationRequest,
+          StatusNotificationResponse
+        >(
+          this,
+          RequestCommand.STATUS_NOTIFICATION,
+          OCPPServiceUtils.buildStatusNotificationRequest(
+            this,
+            reservation.connectorId,
+            ConnectorStatusEnum.Available
+          )
+        );
+        break;
+      }
+    }
+  }
+
+  public getReservationBy(key: string, value: number | string): Reservation {
+    if (this.hasEvses) {
+      for (const evse of this.evses.values()) {
+        for (const connector of evse.connectors.values()) {
+          if (connector?.reservation?.[key] === value) {
+            return connector.reservation;
+          }
+        }
+      }
+    } else {
+      for (const connector of this.connectors.values()) {
+        if (connector?.reservation?.[key] === value) {
+          return connector.reservation;
+        }
+      }
+    }
+  }
+
+  public doesReservationExists(reservation: Partial<Reservation>): [boolean, Reservation] {
+    const foundReservation = this.getReservationBy(
+      ReservationFilterKey.RESERVATION_ID,
+      reservation?.id
+    );
+    return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation];
+  }
+
+  public startReservationExpiryDateSetInterval(customInterval?: number): void {
+    const interval =
+      customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL;
+    logger.info(
+      `${this.logPrefix()} Reservation expiration date interval is set to ${interval}
+        and starts on CS now`
+    );
+    // eslint-disable-next-line @typescript-eslint/no-misused-promises
+    this.reservationExpiryDateSetInterval = setInterval(async (): Promise<void> => {
+      if (this.hasEvses) {
+        for (const evse of this.evses.values()) {
+          for (const connector of evse.connectors.values()) {
+            if (connector?.reservation?.expiryDate.toString() < new Date().toISOString()) {
+              await this.removeReservation(connector.reservation);
+            }
+          }
+        }
+      } else {
+        for (const connector of this.connectors.values()) {
+          if (connector?.reservation?.expiryDate.toString() < new Date().toISOString()) {
+            await this.removeReservation(connector.reservation);
+          }
+        }
+      }
+    }, interval);
+  }
+
+  public restartReservationExpiryDateSetInterval(): void {
+    this.stopReservationExpiryDateSetInterval();
+    this.startReservationExpiryDateSetInterval();
+  }
+
+  public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
+    const reservation = this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId);
+    return !Utils.isUndefined(reservation) && reservation.idTag === idTag;
+  }
+
+  public isConnectorReservable(
+    reservationId: number,
+    idTag?: string,
+    connectorId?: number
+  ): boolean {
+    const [alreadyExists] = this.doesReservationExists({ id: reservationId });
+    if (alreadyExists) {
+      return alreadyExists;
+    }
+    const userReservedAlready = Utils.isUndefined(
+      this.getReservationBy(ReservationFilterKey.ID_TAG, idTag)
+    )
+      ? false
+      : true;
+    const notConnectorZero = Utils.isUndefined(connectorId) ? true : connectorId > 0;
+    const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
+    return !alreadyExists && !userReservedAlready && notConnectorZero && freeConnectorsAvailable;
+  }
+
+  private getNumberOfReservableConnectors(): number {
+    let reservableConnectors = 0;
+    if (this.hasEvses) {
+      for (const evse of this.evses.values()) {
+        reservableConnectors = this.countReservableConnectors(evse.connectors);
+      }
+    } else {
+      reservableConnectors = this.countReservableConnectors(this.connectors);
+    }
+    return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
+  }
+
+  private countReservableConnectors(connectors: Map<number, ConnectorStatus>) {
+    let reservableConnectors = 0;
+    for (const [id, connector] of connectors) {
+      if (id === 0) {
+        continue;
+      }
+      if (connector.status === ConnectorStatusEnum.Available) {
+        ++reservableConnectors;
+      }
+    }
+    return reservableConnectors;
+  }
+
+  private getNumberOfReservationsOnConnectorZero(): number {
+    let numberOfReservations = 0;
+    if (this.hasEvses) {
+      for (const evse of this.evses.values()) {
+        if (evse.connectors.get(0)?.reservation) {
+          ++numberOfReservations;
+        }
+      }
+    } else if (this.connectors.get(0)?.reservation) {
+      ++numberOfReservations;
+    }
+    return numberOfReservations;
+  }
+
   private flushMessageBuffer(): void {
     if (this.messageBuffer.size > 0) {
       for (const message of this.messageBuffer.values()) {
@@ -935,6 +1141,12 @@ export class ChargingStation {
     return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
   }
 
+  private stopReservationExpiryDateSetInterval(): void {
+    if (this.reservationExpiryDateSetInterval) {
+      clearInterval(this.reservationExpiryDateSetInterval);
+    }
+  }
+
   private getSupervisionUrlOcppKey(): string {
     return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
   }
@@ -1083,7 +1295,8 @@ export class ChargingStation {
   }
 
   private handleUnsupportedVersion(version: OCPPVersion) {
-    const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
+    const errorMsg = `Unsupported protocol version '${version}' configured
+      in template file ${this.templateFile}`;
     logger.error(`${this.logPrefix()} ${errorMsg}`);
     throw new BaseError(errorMsg);
   }
index f01034a30029b31e40bbcbb8808326d72203a716..27045c89617c023da1b85d17ee6b8c4b9713e95f 100644 (file)
@@ -24,6 +24,7 @@ import {
   type ClearChargingProfileRequest,
   type ClearChargingProfileResponse,
   type ConnectorStatus,
+  ConnectorStatusEnum,
   ErrorType,
   type GenericResponse,
   GenericStatus,
@@ -35,11 +36,11 @@ import {
   type JsonObject,
   type JsonType,
   OCPP16AuthorizationStatus,
-  type OCPP16AuthorizeRequest,
-  type OCPP16AuthorizeResponse,
   OCPP16AvailabilityType,
   type OCPP16BootNotificationRequest,
   type OCPP16BootNotificationResponse,
+  type OCPP16CancelReservationRequest,
+  type OCPP16CancelReservationResponse,
   OCPP16ChargePointErrorCode,
   OCPP16ChargePointStatus,
   type OCPP16ChargingProfile,
@@ -62,6 +63,8 @@ import {
   OCPP16IncomingRequestCommand,
   OCPP16MessageTrigger,
   OCPP16RequestCommand,
+  type OCPP16ReserveNowRequest,
+  type OCPP16ReserveNowResponse,
   OCPP16StandardParametersKey,
   type OCPP16StartTransactionRequest,
   type OCPP16StartTransactionResponse,
@@ -77,13 +80,17 @@ import {
   OCPPVersion,
   type RemoteStartTransactionRequest,
   type RemoteStopTransactionRequest,
+  ReservationFilterKey,
+  ReservationTerminationReason,
   type ResetRequest,
   type SetChargingProfileRequest,
   type SetChargingProfileResponse,
+  type StartTransactionRequest,
   type UnlockConnectorRequest,
   type UnlockConnectorResponse,
 } from '../../../types';
 import { Constants, Utils, logger } from '../../../utils';
+import { OCPPConstants } from '../OCPPConstants';
 import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService';
 
 const moduleName = 'OCPP16IncomingRequestService';
@@ -137,6 +144,11 @@ 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>>([
       [
@@ -259,6 +271,22 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
           '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'
+        ),
+      ],
     ]);
     this.validatePayload = this.validatePayload.bind(this) as (
       chargingStation: ChargingStation,
@@ -310,7 +338,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         } catch (error) {
           // Log
           logger.error(
-            `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`,
+            `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler:
+              Handle incoming request error:`,
             error
           );
           throw error;
@@ -363,7 +392,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       );
     }
     logger.warn(
-      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation`
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found
+        for command '${commandName}' PDU validation`
     );
     return false;
   }
@@ -384,9 +414,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     logger.info(
       `${chargingStation.logPrefix()} ${
         commandPayload.type
-      } reset command received, simulating it. The station will be back online in ${Utils.formatDurationMilliSeconds(
-        chargingStation.stationInfo.resetTime
-      )}`
+      } reset command received, simulating it. The station will be
+        back online in ${Utils.formatDurationMilliSeconds(chargingStation.stationInfo.resetTime)}`
     );
     return OCPP16Constants.OCPP_RESPONSE_ACCEPTED;
   }
@@ -398,7 +427,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     const connectorId = commandPayload.connectorId;
     if (chargingStation.hasConnector(connectorId) === false) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to unlock a non existing connector id ${connectorId.toString()}`
+        `${chargingStation.logPrefix()} Trying to unlock a non existing
+          connector id ${connectorId.toString()}`
       );
       return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
     }
@@ -545,9 +575,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     }
     if (chargingStation.hasConnector(commandPayload.connectorId) === false) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector id ${
-          commandPayload.connectorId
-        }`
+        `${chargingStation.logPrefix()} Trying to set charging profile(s) to a
+          non existing connector id ${commandPayload.connectorId}`
       );
       return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
     }
@@ -566,9 +595,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
           false)
     ) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${
-          commandPayload.connectorId
-        } without a started transaction`
+        `${chargingStation.logPrefix()} Trying to set transaction charging profile(s)
+          on connector ${commandPayload.connectorId} without a started transaction`
       );
       return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
     }
@@ -601,9 +629,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     }
     if (chargingStation.hasConnector(commandPayload.connectorId) === false) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to get composite schedule to a non existing connector id ${
-          commandPayload.connectorId
-        }`
+        `${chargingStation.logPrefix()} Trying to get composite schedule to a
+          non existing connector id ${commandPayload.connectorId}`
       );
       return OCPP16Constants.OCPP_RESPONSE_REJECTED;
     }
@@ -651,9 +678,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     }
     if (chargingStation.hasConnector(commandPayload.connectorId) === false) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector id ${
-          commandPayload.connectorId
-        }`
+        `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to
+          a non existing connector id ${commandPayload.connectorId}`
       );
       return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
     }
@@ -736,7 +762,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     const connectorId: number = commandPayload.connectorId;
     if (chargingStation.hasConnector(connectorId) === false) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector id ${connectorId.toString()}`
+        `${chargingStation.logPrefix()} Trying to change the availability of a
+          non existing connector id ${connectorId.toString()}`
       );
       return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
     }
@@ -797,27 +824,40 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     chargingStation: ChargingStation,
     commandPayload: RemoteStartTransactionRequest
   ): Promise<GenericResponse> {
-    const transactionConnectorId = commandPayload.connectorId;
+    const { connectorId: transactionConnectorId, idTag, chargingProfile } = commandPayload;
+    const reserved =
+      chargingStation.getConnectorStatus(transactionConnectorId).status ===
+      OCPP16ChargePointStatus.Reserved;
+    const reservedOnConnectorZero =
+      chargingStation.getConnectorStatus(0).status === OCPP16ChargePointStatus.Reserved;
+    if (
+      (reserved &&
+        !chargingStation.validateIncomingRequestWithReservation(transactionConnectorId, idTag)) ||
+      (reservedOnConnectorZero && !chargingStation.validateIncomingRequestWithReservation(0, idTag))
+    ) {
+      return OCPP16Constants.OCPP_RESPONSE_REJECTED;
+    }
     if (chargingStation.hasConnector(transactionConnectorId) === false) {
       return this.notifyRemoteStartTransactionRejected(
         chargingStation,
         transactionConnectorId,
-        commandPayload.idTag
+        idTag
       );
     }
     if (
-      chargingStation.isChargingStationAvailable() === false ||
-      chargingStation.isConnectorAvailable(transactionConnectorId) === false
+      !chargingStation.isChargingStationAvailable() ||
+      !chargingStation.isConnectorAvailable(transactionConnectorId)
     ) {
       return this.notifyRemoteStartTransactionRejected(
         chargingStation,
         transactionConnectorId,
-        commandPayload.idTag
+        idTag
       );
     }
-    const remoteStartTransactionLogMsg = `${chargingStation.logPrefix()} Transaction remotely STARTED on ${
+    const remoteStartTransactionLogMsg = `
+      ${chargingStation.logPrefix()} Transaction remotely STARTED on ${
       chargingStation.stationInfo.chargingStationId
-    }#${transactionConnectorId.toString()} for idTag '${commandPayload.idTag}'`;
+    }#${transactionConnectorId.toString()} for idTag '${idTag}'`;
     await OCPP16ServiceUtils.sendAndSetConnectorStatus(
       chargingStation,
       transactionConnectorId,
@@ -825,77 +865,55 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     );
     const connectorStatus = chargingStation.getConnectorStatus(transactionConnectorId);
     // Check if authorized
-    if (chargingStation.getAuthorizeRemoteTxRequests() === true) {
-      let authorized = false;
+    if (
+      chargingStation.getAuthorizeRemoteTxRequests() &&
+      (await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, transactionConnectorId, idTag))
+    ) {
+      // Authorization successful, start transaction
       if (
-        chargingStation.getLocalAuthListEnabled() === true &&
-        chargingStation.hasIdTags() === true &&
-        Utils.isNotEmptyString(
-          chargingStation.idTagsCache
-            .getIdTags(ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo))
-            ?.find((idTag) => idTag === commandPayload.idTag)
-        )
+        this.setRemoteStartTransactionChargingProfile(
+          chargingStation,
+          transactionConnectorId,
+          chargingProfile
+        ) === true
       ) {
-        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;
+        connectorStatus.transactionRemoteStarted = true;
+        const startTransactionPayload: Partial<StartTransactionRequest> = {
+          connectorId: transactionConnectorId,
+          idTag: idTag,
+        };
+        if (reserved || reservedOnConnectorZero) {
+          const reservation = chargingStation.getReservationBy(
+            ReservationFilterKey.CONNECTOR_ID,
+            reservedOnConnectorZero ? 0 : transactionConnectorId
+          );
+          startTransactionPayload.reservationId = reservation.id;
+          await chargingStation.removeReservation(
+            reservation,
+            ReservationTerminationReason.TRANSACTION_STARTED
+          );
         }
-      } else {
-        logger.warn(
-          `${chargingStation.logPrefix()} The charging station configuration expects authorize at remote start transaction but local authorization or authorize isn't enabled`
-        );
-      }
-      if (authorized === true) {
-        // Authorization successful, start transaction
         if (
-          this.setRemoteStartTransactionChargingProfile(
-            chargingStation,
-            transactionConnectorId,
-            commandPayload.chargingProfile
-          ) === true
+          (
+            await chargingStation.ocppRequestService.requestHandler<
+              OCPP16StartTransactionRequest,
+              OCPP16StartTransactionResponse
+            >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, startTransactionPayload)
+          ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
         ) {
-          connectorStatus.transactionRemoteStarted = true;
-          if (
-            (
-              await chargingStation.ocppRequestService.requestHandler<
-                OCPP16StartTransactionRequest,
-                OCPP16StartTransactionResponse
-              >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
-                connectorId: transactionConnectorId,
-                idTag: commandPayload.idTag,
-              })
-            ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
-          ) {
-            logger.debug(remoteStartTransactionLogMsg);
-            return OCPP16Constants.OCPP_RESPONSE_ACCEPTED;
-          }
-          return this.notifyRemoteStartTransactionRejected(
-            chargingStation,
-            transactionConnectorId,
-            commandPayload.idTag
-          );
+          logger.debug(remoteStartTransactionLogMsg);
+          return OCPP16Constants.OCPP_RESPONSE_ACCEPTED;
         }
         return this.notifyRemoteStartTransactionRejected(
           chargingStation,
           transactionConnectorId,
-          commandPayload.idTag
+          idTag
         );
       }
       return this.notifyRemoteStartTransactionRejected(
         chargingStation,
         transactionConnectorId,
-        commandPayload.idTag
+        idTag
       );
     }
     // No authorization check required, start transaction
@@ -903,7 +921,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       this.setRemoteStartTransactionChargingProfile(
         chargingStation,
         transactionConnectorId,
-        commandPayload.chargingProfile
+        chargingProfile
       ) === true
     ) {
       connectorStatus.transactionRemoteStarted = true;
@@ -914,7 +932,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
             OCPP16StartTransactionResponse
           >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
             connectorId: transactionConnectorId,
-            idTag: commandPayload.idTag,
+            idTag,
           })
         ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
       ) {
@@ -924,13 +942,13 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       return this.notifyRemoteStartTransactionRejected(
         chargingStation,
         transactionConnectorId,
-        commandPayload.idTag
+        idTag
       );
     }
     return this.notifyRemoteStartTransactionRejected(
       chargingStation,
       transactionConnectorId,
-      commandPayload.idTag
+      idTag
     );
   }
 
@@ -949,7 +967,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       );
     }
     logger.warn(
-      `${chargingStation.logPrefix()} Remote starting transaction REJECTED on connector id ${connectorId.toString()}, idTag '${idTag}', availability '${
+      `${chargingStation.logPrefix()} Remote starting transaction REJECTED on connector id
+        ${connectorId.toString()}, idTag '${idTag}', availability '${
         chargingStation.getConnectorStatus(connectorId)?.availability
       }', status '${chargingStation.getConnectorStatus(connectorId)?.status}'`
     );
@@ -964,7 +983,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     if (cp && cp.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE) {
       OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, cp);
       logger.debug(
-        `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on connector id ${connectorId}: %j`,
+        `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction
+          on connector id ${connectorId}: %j`,
         cp
       );
       return true;
@@ -1021,7 +1041,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       }
     }
     logger.warn(
-      `${chargingStation.logPrefix()} Trying to remote stop a non existing transaction with id: ${transactionId.toString()}`
+      `${chargingStation.logPrefix()} Trying to remote stop a non existing transaction with id:
+        ${transactionId.toString()}`
     );
     return OCPP16Constants.OCPP_RESPONSE_REJECTED;
   }
@@ -1038,7 +1059,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       ) === false
     ) {
       logger.warn(
-        `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: feature profile not supported`
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware:
+          Cannot simulate firmware update: feature profile not supported`
       );
       return OCPP16Constants.OCPP_RESPONSE_EMPTY;
     }
@@ -1047,7 +1069,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       chargingStation.stationInfo.firmwareStatus !== OCPP16FirmwareStatus.Installed
     ) {
       logger.warn(
-        `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: firmware update is already in progress`
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware:
+          Cannot simulate firmware update: firmware update is already in progress`
       );
       return OCPP16Constants.OCPP_RESPONSE_EMPTY;
     }
@@ -1146,7 +1169,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       if (runningTransactions > 0) {
         const waitTime = 15 * 1000;
         logger.debug(
-          `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation: ${runningTransactions} transaction(s) in progress, waiting ${
+          `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation:
+            ${runningTransactions} transaction(s) in progress, waiting ${
             waitTime / 1000
           } seconds before continuing firmware update simulation`
         );
@@ -1234,7 +1258,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       ) === false
     ) {
       logger.warn(
-        `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Cannot get diagnostics: feature profile not supported`
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics:
+          Cannot get diagnostics: feature profile not supported`
       );
       return OCPP16Constants.OCPP_RESPONSE_EMPTY;
     }
@@ -1272,9 +1297,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
               })
               .catch((error) => {
                 logger.error(
-                  `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${
-                    OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION
-                  }'`,
+                  `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics:
+                    Error while sending '${OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION}'`,
                   error
                 );
               });
@@ -1499,4 +1523,104 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       );
     }
   }
+
+  private async handleRequestReserveNow(
+    chargingStation: ChargingStation,
+    commandPayload: OCPP16ReserveNowRequest
+  ): Promise<OCPP16ReserveNowResponse> {
+    if (
+      !OCPP16ServiceUtils.checkFeatureProfile(
+        chargingStation,
+        OCPP16SupportedFeatureProfiles.Reservation,
+        OCPP16IncomingRequestCommand.RESERVE_NOW
+      )
+    ) {
+      return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED;
+    }
+    const { reservationId, idTag, connectorId } = commandPayload;
+    let response: OCPP16ReserveNowResponse;
+    try {
+      if (!chargingStation.isConnectorAvailable(connectorId) && connectorId > 0) {
+        return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED;
+      }
+      if (connectorId === 0 && !chargingStation.getReservationOnConnectorId0Enabled()) {
+        return OCPPConstants.OCPP_RESERVATION_RESPONSE_REJECTED;
+      }
+      if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, 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, idTag, connectorId)) {
+            response = OCPPConstants.OCPP_RESERVATION_RESPONSE_OCCUPIED;
+            break;
+          }
+        // eslint-disable-next-line no-fallthrough
+        default:
+          if (!chargingStation.isConnectorReservable(reservationId, idTag)) {
+            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> {
+    if (
+      !OCPP16ServiceUtils.checkFeatureProfile(
+        chargingStation,
+        OCPP16SupportedFeatureProfiles.Reservation,
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION
+      )
+    ) {
+      return OCPPConstants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED;
+    }
+    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 }
+      );
+    }
+  }
 }
index 7ff1fdb8d43b27da48147a789a1612c87644d8a4..b82599b768e08ed13cfff1586ef20d1b12190637 100644 (file)
@@ -24,6 +24,10 @@ import {
   OCPPVersion,
   type RequestParams,
 } from '../../../types';
+import type {
+  OCPP16CancelReservationRequest,
+  OCPP16ReserveNowRequest,
+} from '../../../types/ocpp/1.6/Requests';
 import { Constants, Utils } from '../../../utils';
 import { OCPPRequestService } from '../OCPPRequestService';
 import type { OCPPResponseService } from '../OCPPResponseService';
@@ -119,6 +123,22 @@ export class OCPP16RequestService extends OCPPRequestService {
           'constructor'
         ),
       ],
+      [
+        OCPP16RequestCommand.RESERVE_NOW,
+        OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowRequest>(
+          'assets/json-schemas/ocpp/1.6/ReserveNow.json',
+          moduleName,
+          'constructor'
+        ),
+      ],
+      [
+        OCPP16RequestCommand.CANCEL_RESERVATION,
+        OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationRequest>(
+          'assets/json-schemas/ocpp/1.6/CancelReservation.json',
+          moduleName,
+          'constructor'
+        ),
+      ],
     ]);
     this.buildRequestPayload = this.buildRequestPayload.bind(this) as <Request extends JsonType>(
       chargingStation: ChargingStation,
index 117c36863beefe8184a2bec5e94c7b3d97fbd494..9e8a2d5f7636a33aba59c198a8a7e337cea8f0bf 100644 (file)
@@ -25,6 +25,7 @@ import {
   type OCPP16AuthorizeRequest,
   type OCPP16AuthorizeResponse,
   type OCPP16BootNotificationResponse,
+  type OCPP16CancelReservationResponse,
   OCPP16ChargePointStatus,
   type OCPP16DataTransferResponse,
   type OCPP16DiagnosticsStatusNotificationResponse,
@@ -35,6 +36,7 @@ import {
   type OCPP16MeterValuesRequest,
   type OCPP16MeterValuesResponse,
   OCPP16RequestCommand,
+  type OCPP16ReserveNowResponse,
   OCPP16StandardParametersKey,
   type OCPP16StartTransactionRequest,
   type OCPP16StartTransactionResponse,
@@ -161,6 +163,22 @@ export class OCPP16ResponseService extends OCPPResponseService {
           'constructor'
         ),
       ],
+      [
+        OCPP16RequestCommand.RESERVE_NOW,
+        OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowResponse>(
+          'assets/json-schemas/ocpp/1.6/ReserveNowResponse.json',
+          moduleName,
+          'constructor'
+        ),
+      ],
+      [
+        OCPP16RequestCommand.CANCEL_RESERVATION,
+        OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationResponse>(
+          'assets/json-schemas/ocpp/1.6/CancelReservationResponse.json',
+          moduleName,
+          'constructor'
+        ),
+      ],
     ]);
     this.jsonIncomingRequestResponseSchemas = new Map([
       [
index 23381f9ec3dd63e5e47faed0301fc6fecbc01f75..c73c32dc2ce940f9c017550183a44dd45a3a7a43 100644 (file)
@@ -2,7 +2,7 @@
 
 import type { JSONSchemaType } from 'ajv';
 
-import type { ChargingStation } from '../../../charging-station';
+import { type ChargingStation, ChargingStationUtils } from '../../../charging-station';
 import { OCPPError } from '../../../exception';
 import {
   CurrentType,
@@ -13,6 +13,9 @@ import {
   MeterValueContext,
   MeterValueLocation,
   MeterValueUnit,
+  OCPP16AuthorizationStatus,
+  type OCPP16AuthorizeRequest,
+  type OCPP16AuthorizeResponse,
   type OCPP16ChargingProfile,
   type OCPP16IncomingRequestCommand,
   type OCPP16MeterValue,
@@ -837,6 +840,30 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     );
   }
 
+  public static async isIdTagAuthorized(
+    chargingStation: ChargingStation,
+    connectorId: number,
+    idTag: string,
+    parentIdTag?: string
+  ): Promise<boolean> {
+    let authorized = false;
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+    if (OCPP16ServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)) {
+      connectorStatus.localAuthorizeIdTag = idTag;
+      connectorStatus.idTagLocalAuthorized = true;
+      authorized = true;
+    } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) {
+      connectorStatus.authorizeIdTag = idTag;
+      authorized = await OCPP16ServiceUtils.isIdTagRemoteAuthorized(chargingStation, idTag);
+    } 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 static buildSampledValue(
     sampledValueTemplate: SampledValueTemplate,
     value: number,
@@ -912,4 +939,30 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
         return MeterValueUnit.VOLT;
     }
   }
+
+  private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
+    return (
+      chargingStation.getLocalAuthListEnabled() === true &&
+      chargingStation.hasIdTags() === true &&
+      Utils.isNotEmptyString(
+        chargingStation.idTagsCache
+          .getIdTags(ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo))
+          ?.find((tag) => tag === idTag)
+      )
+    );
+  }
+
+  private static async isIdTagRemoteAuthorized(
+    chargingStation: ChargingStation,
+    idTag: string
+  ): Promise<boolean> {
+    const authorizeResponse: OCPP16AuthorizeResponse =
+      await chargingStation.ocppRequestService.requestHandler<
+        OCPP16AuthorizeRequest,
+        OCPP16AuthorizeResponse
+      >(chargingStation, OCPP16RequestCommand.AUTHORIZE, {
+        idTag: idTag,
+      });
+    return authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED;
+  }
 }
index e82b7d70a6704110df3950baeb059e01df9ce28a..b3f4dd3208315681d616f10cacfafa8bfb29ea77 100644 (file)
@@ -9,6 +9,7 @@ import {
   TriggerMessageStatus,
   UnlockStatus,
 } from '../../types';
+import { ReservationStatus } from '../../types/ocpp/Responses';
 import { Constants } from '../../utils';
 
 export class OCPPConstants {
@@ -104,11 +105,39 @@ export class OCPPConstants {
   static readonly OCPP_DATA_TRANSFER_RESPONSE_REJECTED = Object.freeze({
     status: DataTransferStatus.REJECTED,
   });
-
+  
   static readonly OCPP_DATA_TRANSFER_RESPONSE_UNKNOWN_VENDOR_ID = Object.freeze({
     status: DataTransferStatus.UNKNOWN_VENDOR_ID,
   });
 
+  static readonly OCPP_RESERVATION_RESPONSE_ACCEPTED = Object.freeze({
+    status: ReservationStatus.ACCEPTED,
+  }); // Reservation has been made
+
+  static readonly OCPP_RESERVATION_RESPONSE_FAULTED = Object.freeze({
+    status: ReservationStatus.FAULTED,
+  }); // Reservation has not been made, because of connector in FAULTED state
+
+  static readonly OCPP_RESERVATION_RESPONSE_OCCUPIED = Object.freeze({
+    status: ReservationStatus.OCCUPIED,
+  }); // Reservation has not been made, because all connectors are OCCUPIED
+
+  static readonly OCPP_RESERVATION_RESPONSE_REJECTED = Object.freeze({
+    status: ReservationStatus.REJECTED,
+  }); // Reservation has not been made, because CS is not configured to accept reservations
+
+  static readonly OCPP_RESERVATION_RESPONSE_UNAVAILABLE = Object.freeze({
+    status: ReservationStatus.UNAVAILABLE,
+  }); // Reservation has not been made, because connectors are spec. connector is in UNAVAILABLE state
+
+  static readonly OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED = Object.freeze({
+    status: GenericStatus.Accepted,
+  }); // Reservation for id has been cancelled has been made
+
+  static readonly OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED = Object.freeze({
+    status: GenericStatus.Rejected,
+  }); // Reservation could not be cancelled, because there is no reservation active for id
+
   protected constructor() {
     // This is intentional
   }
index d7698107b9cc2146677db9624f66ae174d8c9dd8..33c72242b5e1983c4df4681295d6933a8cf2180a 100644 (file)
@@ -3,6 +3,7 @@ import type { ChargingProfile } from './ocpp/ChargingProfile';
 import type { ConnectorStatusEnum } from './ocpp/ConnectorStatusEnum';
 import type { MeterValue } from './ocpp/MeterValues';
 import type { AvailabilityType } from './ocpp/Requests';
+import type { Reservation } from './ocpp/Reservation';
 
 export type ConnectorStatus = {
   availability: AvailabilityType;
@@ -22,4 +23,5 @@ export type ConnectorStatus = {
   transactionEnergyActiveImportRegisterValue?: number; // In Wh
   transactionBeginMeterValue?: MeterValue;
   chargingProfiles?: ChargingProfile[];
+  reservation?: Reservation;
 };
index fee7eab631dc797b16b1156d6cbaa5f8605f9bf1..7bfa2150abfaf1c26927024b6250d617541b24db 100644 (file)
@@ -108,6 +108,8 @@ export {
   type ResetRequest,
   type SetChargingProfileRequest,
   type UnlockConnectorRequest,
+  type OCPP16ReserveNowRequest,
+  type OCPP16CancelReservationRequest,
 } from './ocpp/1.6/Requests';
 export {
   type ChangeAvailabilityResponse,
@@ -127,6 +129,8 @@ export {
   type OCPP16UpdateFirmwareResponse,
   type SetChargingProfileResponse,
   type UnlockConnectorResponse,
+  type OCPP16ReserveNowResponse,
+  type OCPP16CancelReservationResponse,
 } from './ocpp/1.6/Responses';
 export { ChargePointErrorCode } from './ocpp/ChargePointErrorCode';
 export {
@@ -251,3 +255,5 @@ export {
   WebSocketCloseEventStatusCode,
   WebSocketCloseEventStatusString,
 } from './WebSocket';
+export { ReservationFilterKey, ReservationTerminationReason } from './ocpp/1.6/Reservation';
+export { type Reservation } from './ocpp/Reservation';
index 60c1787fefe5e06972a72dfcd5ba730e013f3350..80128a271c9fd6edd3cb6ec9609ad6b2ed898da7 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;
@@ -183,3 +187,15 @@ export interface OCPP16DataTransferRequest extends JsonObject {
   messageId?: string;
   data?: string;
 }
+
+export interface OCPP16ReserveNowRequest extends JsonObject {
+  connectorId: number;
+  expiryDate: Date;
+  idTag: string;
+  parentIdTag?: string;
+  reservationId: number;
+}
+
+export interface OCPP16CancelReservationRequest extends JsonObject {
+  reservationId: number;
+}
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..bbc3de7
--- /dev/null
@@ -0,0 +1,22 @@
+export interface OCPP16Reservation {
+  id: number;
+  connectorId: number;
+  expiryDate: Date;
+  idTag: string;
+  parentIdTag?: string;
+}
+
+export enum ReservationTerminationReason {
+  EXPIRED = 'Expired',
+  TRANSACTION_STARTED = 'TransactionStarted',
+  CONNECTOR_STATE_CHANGED = 'ConnectorStateChanged',
+  RESERVATION_CANCELED = 'ReservationCanceled',
+}
+
+export enum ReservationFilterKey {
+  RESERVATION_ID = 'id',
+  ID_TAG = 'idTag',
+  PARENT_ID_TAG = 'parentIdTag',
+  CONNECTOR_ID = 'connectorId',
+  EVSE_ID = 'evseId',
+}
index a6683caa87962b9050f6ef9fa1d4e7b8a3b48f2c..1a163aa82b72745c7ab42b560dae78c90f2642f5 100644 (file)
@@ -2,7 +2,7 @@ import type { OCPP16ChargingSchedule } from './ChargingProfile';
 import type { EmptyObject } from '../../EmptyObject';
 import type { JsonObject } from '../../JsonType';
 import type { OCPPConfigurationKey } from '../Configuration';
-import type { GenericStatus, RegistrationStatusEnumType } from '../Responses';
+import { GenericStatus, type RegistrationStatusEnumType } from '../Responses';
 
 export interface OCPP16HeartbeatResponse extends JsonObject {
   currentTime: Date;
@@ -109,3 +109,20 @@ export interface OCPP16DataTransferResponse extends JsonObject {
   status: OCPP16DataTransferStatus;
   data?: string;
 }
+
+export interface OCPP16CancelReservationResponse extends JsonObject {
+  status: GenericStatus;
+}
+
+export enum OCPP16ReservationStatus {
+  ACCEPTED = 'Accepted',
+  FAULTED = 'Faulted',
+  OCCUPIED = 'Occupied',
+  REJECTED = 'Rejected',
+  UNAVAILABLE = 'Unavailable',
+  NOT_SUPPORTED = 'NotSupported',
+}
+
+export interface OCPP16ReserveNowResponse extends JsonObject {
+  status: OCPP16ReservationStatus;
+}
index 7eef7b5b14d35f2db1c3aebef13d8f1ad12aaf82..9ca43da713e29ad1112b074ab1dd78dc49e6f123 100644 (file)
@@ -3,6 +3,7 @@ import type { OCPP16MeterValuesRequest } from './1.6/MeterValues';
 import {
   OCPP16AvailabilityType,
   type OCPP16BootNotificationRequest,
+  type OCPP16CancelReservationRequest,
   type OCPP16DataTransferRequest,
   type OCPP16DiagnosticsStatusNotificationRequest,
   OCPP16FirmwareStatus,
@@ -11,6 +12,7 @@ import {
   OCPP16IncomingRequestCommand,
   OCPP16MessageTrigger,
   OCPP16RequestCommand,
+  type OCPP16ReserveNowRequest,
   type OCPP16StatusNotificationRequest,
 } from './1.6/Requests';
 import { OperationalStatusEnumType } from './2.0/Common';
@@ -101,3 +103,7 @@ export const FirmwareStatus = {
 export type FirmwareStatus = OCPP16FirmwareStatus;
 
 export type ResponseType = JsonType | OCPPError;
+
+export type ReserveNowRequest = OCPP16ReserveNowRequest;
+
+export type CancelReservationRequest = OCPP16CancelReservationRequest;
diff --git a/src/types/ocpp/Reservation.ts b/src/types/ocpp/Reservation.ts
new file mode 100644 (file)
index 0000000..b185b87
--- /dev/null
@@ -0,0 +1,3 @@
+import { type OCPP16Reservation } from './1.6/Reservation';
+
+export type Reservation = OCPP16Reservation;
index 24b8b848bebe9656660c4c884af6faf5a75df932..8735dc8ae10f2cf72362fe0ee9388a773794a9c2 100644 (file)
@@ -2,6 +2,7 @@ import type { OCPP16MeterValuesResponse } from './1.6/MeterValues';
 import {
   OCPP16AvailabilityStatus,
   type OCPP16BootNotificationResponse,
+  type OCPP16CancelReservationResponse,
   OCPP16ChargingProfileStatus,
   OCPP16ClearChargingProfileStatus,
   OCPP16ConfigurationStatus,
@@ -10,6 +11,7 @@ import {
   type OCPP16DiagnosticsStatusNotificationResponse,
   type OCPP16FirmwareStatusNotificationResponse,
   type OCPP16HeartbeatResponse,
+  OCPP16ReservationStatus,
   type OCPP16StatusNotificationResponse,
   OCPP16TriggerMessageStatus,
   OCPP16UnlockStatus,
@@ -103,3 +105,14 @@ export const DataTransferStatus = {
   ...OCPP16DataTransferStatus,
 } as const;
 export type DataTransferStatus = OCPP16DataTransferStatus;
+
+export type ReservationStatus = OCPP16ReservationStatus;
+export const ReservationStatus = {
+  ...OCPP16ReservationStatus,
+};
+
+export const CancelReservationStatus = {
+  ...GenericStatus,
+};
+
+export type CancelReservationResponse = OCPP16CancelReservationResponse;
index a5cd81502d230e447a96efb4084b66ab59e2af1c..bb765c75dcf8f00d6ed766c1c1f8ccdcd1592357 100644 (file)
@@ -52,6 +52,8 @@ export class Constants {
     /* This is intentional */
   });
 
+  static readonly DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL = 5000; // Ms
+
   private constructor() {
     // This is intentional
   }