fix: fix reservationId payload field filling at start transaction
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 31 Jul 2023 17:26:53 +0000 (19:26 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 31 Jul 2023 17:26:53 +0000 (19:26 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/ChargingStation.ts
src/charging-station/Helpers.ts
src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts
src/charging-station/index.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/ui-server/UIServerUtils.ts
src/utils/Constants.ts

index d3b53bd22cfe7071a3c3c565b3dbf0b52d501af5..b6aa7b98137a085eb9f4539b14edd8d50984ea59 100644 (file)
@@ -31,7 +31,6 @@ import {
   checkConnectorsConfiguration,
   checkStationInfoConnectorStatus,
   checkTemplate,
-  countReservableConnectors,
   createBootNotificationRequest,
   createSerialNumber,
   getAmperageLimitationUnitDivider,
@@ -42,10 +41,13 @@ import {
   getHashId,
   getIdTagsFile,
   getMaxNumberOfEvses,
+  getNumberOfReservableConnectors,
   getPhaseRotationValue,
   hasFeatureProfile,
+  hasReservationExpired,
   initializeConnectorsMapStatus,
   propagateSerialNumber,
+  removeExpiredReservations,
   stationTemplateToStationInfo,
   warnTemplateKeysDeprecation,
 } from './Helpers';
@@ -962,7 +964,7 @@ export class ChargingStation {
 
   public async removeReservation(
     reservation: Reservation,
-    reason?: ReservationTerminationReason,
+    reason: ReservationTerminationReason,
   ): Promise<void> {
     const connector = this.getConnectorStatus(reservation.connectorId)!;
     switch (reason) {
@@ -983,7 +985,8 @@ export class ChargingStation {
         delete connector.reservation;
         break;
       default:
-        break;
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        throw new Error(`Unknown reservation termination reason '${reason}'`);
     }
   }
 
@@ -1013,12 +1016,16 @@ export class ChargingStation {
     idTag?: string,
     connectorId?: number,
   ): boolean {
-    const reservationExists = !isUndefined(this.getReservationBy('reservationId', reservationId));
+    const reservation = this.getReservationBy('reservationId', reservationId);
+    const reservationExists = !isUndefined(reservation) && !hasReservationExpired(reservation!);
     if (arguments.length === 1) {
       return !reservationExists;
     } else if (arguments.length > 1) {
+      const userReservation = !isUndefined(idTag)
+        ? this.getReservationBy('idTag', idTag!)
+        : undefined;
       const userReservationExists =
-        !isUndefined(idTag) && isUndefined(this.getReservationBy('idTag', idTag!)) ? false : true;
+        !isUndefined(userReservation) && !hasReservationExpired(userReservation!);
       const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
       const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
       return (
@@ -1038,34 +1045,7 @@ export class ChargingStation {
         )}`,
       );
       this.reservationExpirationSetInterval = setInterval((): void => {
-        const currentDate = new Date();
-        if (this.hasEvses) {
-          for (const evseStatus of this.evses.values()) {
-            for (const connectorStatus of evseStatus.connectors.values()) {
-              if (
-                connectorStatus.reservation &&
-                connectorStatus.reservation.expiryDate < currentDate
-              ) {
-                this.removeReservation(
-                  connectorStatus.reservation,
-                  ReservationTerminationReason.EXPIRED,
-                ).catch(Constants.EMPTY_FUNCTION);
-              }
-            }
-          }
-        } else {
-          for (const connectorStatus of this.connectors.values()) {
-            if (
-              connectorStatus.reservation &&
-              connectorStatus.reservation.expiryDate < currentDate
-            ) {
-              this.removeReservation(
-                connectorStatus.reservation,
-                ReservationTerminationReason.EXPIRED,
-              ).catch(Constants.EMPTY_FUNCTION);
-            }
-          }
-        }
+        removeExpiredReservations(this).catch(Constants.EMPTY_FUNCTION);
       }, interval);
     }
   }
@@ -1082,15 +1062,15 @@ export class ChargingStation {
   // }
 
   private getNumberOfReservableConnectors(): number {
-    let reservableConnectors = 0;
+    let numberOfReservableConnectors = 0;
     if (this.hasEvses) {
       for (const evseStatus of this.evses.values()) {
-        reservableConnectors += countReservableConnectors(evseStatus.connectors);
+        numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors);
       }
     } else {
-      reservableConnectors = countReservableConnectors(this.connectors);
+      numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors);
     }
-    return reservableConnectors - this.getNumberOfReservationsOnConnectorZero();
+    return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero();
   }
 
   private getNumberOfReservationsOnConnectorZero(): number {
index 6cd270ffee29855891f0d79a6565af6547283c85..d2b1b926bdf8e4bfb51610b8d71e75f81eef69ec 100644 (file)
@@ -14,6 +14,7 @@ import {
   isAfter,
   isBefore,
   isDate,
+  isPast,
   isWithinInterval,
   toDate,
 } from 'date-fns';
@@ -42,6 +43,8 @@ import {
   type OCPP20BootNotificationRequest,
   OCPPVersion,
   RecurrencyKindType,
+  type Reservation,
+  ReservationTerminationReason,
   StandardParametersKey,
   SupportedFeatureProfiles,
   Voltage,
@@ -82,7 +85,39 @@ export const getChargingStationId = (
       )}${idSuffix}`;
 };
 
-export const countReservableConnectors = (connectors: Map<number, ConnectorStatus>) => {
+export const hasReservationExpired = (reservation: Reservation): boolean => {
+  return isPast(reservation.expiryDate);
+};
+
+export const removeExpiredReservations = async (
+  chargingStation: ChargingStation,
+): Promise<void> => {
+  if (chargingStation.hasEvses) {
+    for (const evseStatus of chargingStation.evses.values()) {
+      for (const connectorStatus of evseStatus.connectors.values()) {
+        if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
+          await chargingStation.removeReservation(
+            connectorStatus.reservation,
+            ReservationTerminationReason.EXPIRED,
+          );
+        }
+      }
+    }
+  } else {
+    for (const connectorStatus of chargingStation.connectors.values()) {
+      if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
+        await chargingStation.removeReservation(
+          connectorStatus.reservation,
+          ReservationTerminationReason.EXPIRED,
+        );
+      }
+    }
+  }
+};
+
+export const getNumberOfReservableConnectors = (
+  connectors: Map<number, ConnectorStatus>,
+): number => {
   let reservableConnectors = 0;
   for (const [connectorId, connectorStatus] of connectors) {
     if (connectorId === 0) {
index 83684c647f8cfb9d11689fc16fd472b6ddd6886f..fff823c09b22989f441c6b88968018a6acc933a6 100644 (file)
@@ -317,7 +317,7 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
       this.cleanRequestPayload(command, requestPayload);
       return this.commandHandlers.get(command)!(requestPayload);
     }
-    throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
+    throw new BaseError(`Unknown worker broadcast channel command: '${command}'`);
   }
 
   private cleanRequestPayload(
index 0d15d676495b35fe070171ecc675ac34074477d4..e1627f9917ea8a10f1a88786524d17f59ba57ffe 100644 (file)
@@ -6,8 +6,10 @@ export {
   setConfigurationKeyValue,
 } from './ConfigurationKeyUtils';
 export {
-  getIdTagsFile,
   checkChargingStation,
-  resetConnectorStatus,
+  getIdTagsFile,
   hasFeatureProfile,
+  hasReservationExpired,
+  removeExpiredReservations,
+  resetConnectorStatus,
 } from './Helpers';
index 567c317931d927421f05cbe10e1470e370da3a63..7a9e1a4ec2b2909cc73c5380d7316c4bd2373dba 100644 (file)
@@ -15,6 +15,7 @@ import {
   type ChargingStation,
   checkChargingStation,
   getConfigurationKey,
+  removeExpiredReservations,
   setConfigurationKeyValue,
 } from '../../../charging-station';
 import { OCPPError } from '../../../exception';
@@ -833,15 +834,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         idTag,
       );
     }
-    if (
-      (chargingStation.getConnectorStatus(transactionConnectorId)?.status ===
-        OCPP16ChargePointStatus.Reserved &&
-        chargingStation.getReservationBy('connectorId', transactionConnectorId)?.idTag !== idTag) ||
-      (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
-        chargingStation.getReservationBy('connectorId', 0)?.idTag !== idTag)
-    ) {
-      return OCPP16Constants.OCPP_RESPONSE_REJECTED;
-    }
     const remoteStartTransactionLogMsg = `
       ${chargingStation.logPrefix()} Transaction remotely STARTED on ${
         chargingStation.stationInfo.chargingStationId
@@ -874,12 +866,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
             >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
               connectorId: transactionConnectorId,
               idTag,
-              reservationId: chargingStation.getReservationBy(
-                'connectorId',
-                chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved
-                  ? 0
-                  : transactionConnectorId,
-              )!,
             })
           ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
         ) {
@@ -915,12 +901,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
           >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
             connectorId: transactionConnectorId,
             idTag,
-            reservationId: chargingStation.getReservationBy(
-              'connectorId',
-              chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved
-                ? 0
-                : transactionConnectorId,
-            )!,
           })
         ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
       ) {
@@ -1527,6 +1507,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) {
         return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED;
       }
+      await removeExpiredReservations(chargingStation);
       switch (chargingStation.getConnectorStatus(connectorId)!.status) {
         case OCPP16ChargePointStatus.Faulted:
           response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED;
@@ -1588,8 +1569,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       const { reservationId } = commandPayload;
       const reservation = chargingStation.getReservationBy('reservationId', reservationId);
       if (isUndefined(reservation)) {
-        logger.error(
-          `${chargingStation.logPrefix()} Reservation with ID ${reservationId}
+        logger.debug(
+          `${chargingStation.logPrefix()} Reservation with id ${reservationId}
             does not exist on charging station`,
         );
         return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED;
index ac1c1ac30d8810ac85e00de9b94b438e4e9876a0..5c1c129f6b671a96d22207f8aa4c6620a88ab7d1 100644 (file)
@@ -12,6 +12,7 @@ import {
   type JsonType,
   type OCPP16AuthorizeRequest,
   type OCPP16BootNotificationRequest,
+  OCPP16ChargePointStatus,
   type OCPP16DataTransferRequest,
   type OCPP16DiagnosticsStatusNotificationRequest,
   type OCPP16FirmwareStatusNotificationRequest,
@@ -183,6 +184,18 @@ export class OCPP16RequestService extends OCPPRequestService {
             true,
           ),
           timestamp: new Date(),
+          ...(OCPP16ServiceUtils.hasReservation(
+            chargingStation,
+            commandParams?.connectorId as number,
+            commandParams?.idTag as string,
+          ) && {
+            reservationId: chargingStation.getReservationBy(
+              'connectorId',
+              chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved
+                ? 0
+                : (commandParams?.connectorId as number),
+            )!.reservationId,
+          }),
           ...commandParams,
         } as unknown as Request;
       case OCPP16RequestCommand.STOP_TRANSACTION:
index ae8b6287bd8757e01b134cfd2e50a67167bae383..f1a17068bc31ee7761fe9752eaa788de32c844ed 100644 (file)
@@ -10,6 +10,7 @@ import {
   type ChargingStation,
   addConfigurationKey,
   getConfigurationKey,
+  hasReservationExpired,
   resetConnectorStatus,
 } from '../../../charging-station';
 import { OCPPError } from '../../../exception';
@@ -639,18 +640,31 @@ export class OCPP16ResponseService extends OCPPResponseService {
           transactionConnectorId,
           requestPayload.meterStart,
         );
-      const reservedOnConnectorZero =
-        chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved;
-      if (
-        chargingStation.getConnectorStatus(transactionConnectorId)?.status ===
-          OCPP16ChargePointStatus.Reserved ||
-        reservedOnConnectorZero
-      ) {
+      if (requestPayload.reservationId) {
+        const reservation = chargingStation.getReservationBy(
+          'reservationId',
+          requestPayload.reservationId,
+        )!;
+        if (reservation.idTag !== requestPayload.idTag) {
+          logger.warn(
+            `${chargingStation.logPrefix()} Transaction reserved ${
+              payload.transactionId
+            } started with a different idTag ${requestPayload.idTag} than the reservation one ${
+              reservation.idTag
+            }`,
+          );
+        }
+        if (hasReservationExpired(reservation)) {
+          logger.warn(
+            `${chargingStation.logPrefix()} Transaction reserved ${
+              payload.transactionId
+            } started with expired reservation ${
+              requestPayload.reservationId
+            } (expiry date: ${reservation.expiryDate.toISOString()}))`,
+          );
+        }
         await chargingStation.removeReservation(
-          chargingStation.getReservationBy(
-            'connectorId',
-            reservedOnConnectorZero ? 0 : transactionConnectorId,
-          )!,
+          reservation,
           ReservationTerminationReason.TRANSACTION_STARTED,
         );
       }
index b70be6fadc8170728fcd06abcda9d88e6e73afc7..b6229b90fb94960e88af423b24ebef1395fb3dc0 100644 (file)
@@ -3,7 +3,11 @@
 import type { JSONSchemaType } from 'ajv';
 
 import { OCPP16Constants } from './OCPP16Constants';
-import { type ChargingStation, hasFeatureProfile } from '../../../charging-station';
+import {
+  type ChargingStation,
+  hasFeatureProfile,
+  hasReservationExpired,
+} from '../../../charging-station';
 import { OCPPError } from '../../../exception';
 import {
   type ClearChargingProfileRequest,
@@ -924,6 +928,29 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     return clearedCP;
   };
 
+  public static hasReservation = (
+    chargingStation: ChargingStation,
+    connectorId: number,
+    idTag: string,
+  ): boolean => {
+    const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId);
+    const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0);
+    if (
+      (chargingStation.getConnectorStatus(connectorId)?.status ===
+        OCPP16ChargePointStatus.Reserved &&
+        connectorReservation &&
+        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+        (hasReservationExpired(connectorReservation) || connectorReservation?.idTag !== idTag)) ||
+      (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
+        chargingStationReservation &&
+        (hasReservationExpired(chargingStationReservation) ||
+          chargingStationReservation?.idTag !== idTag))
+    ) {
+      return false;
+    }
+    return true;
+  };
+
   public static parseJsonSchemaFile<T extends JsonType>(
     relativePath: string,
     moduleName?: string,
index 79f8c34a74d5eec236dc8ab84aa672617b2f0daf..95bbba56c8134dd93f20ebbf2dcf06986e70961d 100644 (file)
@@ -26,7 +26,7 @@ export class UIServerUtils {
     logger.error(
       `${logPrefix(
         ' UI WebSocket Server |',
-      )} Unsupported protocol: ${protocol} or protocol version: ${version}`,
+      )} Unsupported protocol: '${protocol}' or protocol version: '${version}'`,
     );
     return false;
   };
index c4ce0ff8c48e30b8faefd22a118c1718ec74505a..8ab45e63e1293a9e0ccbd662a0f3e23deb0f004c 100644 (file)
@@ -55,7 +55,7 @@ export class Constants {
     /* This is intentional */
   });
 
-  static readonly DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL = 5000; // Ms
+  static readonly DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL = 60000; // Ms
 
   private constructor() {
     // This is intentional