fix: fix reservationId payload field filling at start transaction
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
index 0f2f5217956100920f6c1377e21ad5b6105017dd..b6aa7b98137a085eb9f4539b14edd8d50984ea59 100644 (file)
@@ -14,6 +14,7 @@ import { dirname, join } from 'node:path';
 import { URL } from 'node:url';
 import { parentPort } from 'node:worker_threads';
 
+import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns';
 import merge from 'just-merge';
 import { type RawData, WebSocket } from 'ws';
 
@@ -24,13 +25,12 @@ import {
   deleteConfigurationKey,
   getConfigurationKey,
   setConfigurationKeyValue,
-} from './ChargingStationConfigurationUtils';
+} from './ConfigurationKeyUtils';
 import {
   buildConnectorsMap,
   checkConnectorsConfiguration,
   checkStationInfoConnectorStatus,
   checkTemplate,
-  countReservableConnectors,
   createBootNotificationRequest,
   createSerialNumber,
   getAmperageLimitationUnitDivider,
@@ -41,12 +41,16 @@ import {
   getHashId,
   getIdTagsFile,
   getMaxNumberOfEvses,
+  getNumberOfReservableConnectors,
   getPhaseRotationValue,
+  hasFeatureProfile,
+  hasReservationExpired,
   initializeConnectorsMapStatus,
   propagateSerialNumber,
+  removeExpiredReservations,
   stationTemplateToStationInfo,
   warnTemplateKeysDeprecation,
-} from './ChargingStationUtils';
+} from './Helpers';
 import { IdTagsCache } from './IdTagsCache';
 import {
   OCPP16IncomingRequestService,
@@ -102,7 +106,7 @@ import {
   RegistrationStatusEnumType,
   RequestCommand,
   type Reservation,
-  ReservationFilterKey,
+  type ReservationKey,
   ReservationTerminationReason,
   type Response,
   StandardParametersKey,
@@ -244,8 +248,8 @@ export class ChargingStation {
     return this.stationInfo.enableStatistics ?? false;
   }
 
-  public getMustAuthorizeAtRemoteStart(): boolean {
-    return this.stationInfo.mustAuthorizeAtRemoteStart ?? true;
+  public getRemoteAuthorization(): boolean {
+    return this.stationInfo.remoteAuthorization ?? true;
   }
 
   public getNumberOfPhases(stationInfo?: ChargingStationInfo): number {
@@ -516,11 +520,11 @@ export class ChargingStation {
   public getHeartbeatInterval(): number {
     const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval);
     if (HeartbeatInterval) {
-      return convertToInt(HeartbeatInterval.value) * 1000;
+      return secondsToMilliseconds(convertToInt(HeartbeatInterval.value));
     }
     const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval);
     if (HeartBeatInterval) {
-      return convertToInt(HeartBeatInterval.value) * 1000;
+      return secondsToMilliseconds(convertToInt(HeartBeatInterval.value));
     }
     this.stationInfo?.autoRegister === false &&
       logger.warn(
@@ -592,14 +596,14 @@ export class ChargingStation {
   public startMeterValues(connectorId: number, interval: number): void {
     if (connectorId === 0) {
       logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}`,
+        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`,
       );
       return;
     }
     if (!this.getConnectorStatus(connectorId)) {
       logger.error(
         `${this.logPrefix()} Trying to start MeterValues on non existing connector id
-          ${connectorId.toString()}`,
+          ${connectorId}`,
       );
       return;
     }
@@ -667,7 +671,7 @@ export class ChargingStation {
         if (this.getEnableStatistics() === true) {
           this.performanceStatistics?.start();
         }
-        if (this.hasFeatureProfile(SupportedFeatureProfiles.Reservation)) {
+        if (hasFeatureProfile(this, SupportedFeatureProfiles.Reservation)) {
           this.startReservationExpirationSetInterval();
         }
         this.openWSConnection();
@@ -730,6 +734,9 @@ export class ChargingStation {
         if (this.getEnableStatistics() === true) {
           this.performanceStatistics?.stop();
         }
+        if (hasFeatureProfile(this, SupportedFeatureProfiles.Reservation)) {
+          this.stopReservationExpirationSetInterval();
+        }
         this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
         this.templateFileWatcher?.close();
         this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
@@ -759,13 +766,6 @@ export class ChargingStation {
     }
   }
 
-  public hasFeatureProfile(featureProfile: SupportedFeatureProfiles): boolean | undefined {
-    return getConfigurationKey(
-      this,
-      StandardParametersKey.SupportedFeatureProfiles,
-    )?.value?.includes(featureProfile);
-  }
-
   public bufferMessage(message: string): void {
     this.messageBuffer.add(message);
   }
@@ -777,7 +777,7 @@ export class ChargingStation {
       terminateOpened: false,
     },
   ): void {
-    options = { handshakeTimeout: this.getConnectionTimeout() * 1000, ...options };
+    options = { handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()), ...options };
     params = { ...{ closeOpened: false, terminateOpened: false }, ...params };
     if (this.started === false && this.starting === false) {
       logger.warn(
@@ -938,15 +938,15 @@ export class ChargingStation {
     );
   }
 
-  public getReservationOnConnectorId0Enabled(): boolean {
+  public getReserveConnectorZeroSupported(): boolean {
     return convertToBoolean(
       getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value,
     );
   }
 
   public async addReservation(reservation: Reservation): Promise<void> {
-    const [exists, reservationFound] = this.doesReservationExists(reservation);
-    if (exists) {
+    const reservationFound = this.getReservationBy('reservationId', reservation.reservationId);
+    if (!isUndefined(reservationFound)) {
       await this.removeReservation(
         reservationFound!,
         ReservationTerminationReason.REPLACE_EXISTING,
@@ -964,13 +964,11 @@ export class ChargingStation {
 
   public async removeReservation(
     reservation: Reservation,
-    reason?: ReservationTerminationReason,
+    reason: ReservationTerminationReason,
   ): Promise<void> {
     const connector = this.getConnectorStatus(reservation.connectorId)!;
     switch (reason) {
       case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
-        delete connector.reservation;
-        break;
       case ReservationTerminationReason.TRANSACTION_STARTED:
         delete connector.reservation;
         break;
@@ -987,127 +985,101 @@ 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}'`);
     }
   }
 
   public getReservationBy(
-    filterKey: ReservationFilterKey,
+    filterKey: ReservationKey,
     value: number | string,
   ): Reservation | undefined {
     if (this.hasEvses) {
       for (const evseStatus of this.evses.values()) {
         for (const connectorStatus of evseStatus.connectors.values()) {
-          if (connectorStatus?.reservation?.[filterKey as keyof Reservation] === value) {
+          if (connectorStatus?.reservation?.[filterKey] === value) {
             return connectorStatus.reservation;
           }
         }
       }
     } else {
       for (const connectorStatus of this.connectors.values()) {
-        if (connectorStatus?.reservation?.[filterKey as keyof Reservation] === value) {
+        if (connectorStatus?.reservation?.[filterKey] === value) {
           return connectorStatus.reservation;
         }
       }
     }
   }
 
-  public doesReservationExists(
-    reservation: Partial<Reservation>,
-  ): [boolean, Reservation | undefined] {
-    const foundReservation = this.getReservationBy(
-      ReservationFilterKey.RESERVATION_ID,
-      reservation.id!,
-    );
-    return isUndefined(foundReservation) ? [false, undefined] : [true, foundReservation];
+  public isConnectorReservable(
+    reservationId: number,
+    idTag?: string,
+    connectorId?: number,
+  ): boolean {
+    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(userReservation) && !hasReservationExpired(userReservation!);
+      const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
+      const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
+      return (
+        !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
+      );
+    }
+    return false;
   }
 
-  public startReservationExpirationSetInterval(customInterval?: number): void {
+  private startReservationExpirationSetInterval(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 charging station now`,
-    );
     if (interval > 0) {
+      logger.info(
+        `${this.logPrefix()} Reservation expiration date checks started every ${formatDurationMilliSeconds(
+          interval,
+        )}`,
+      );
       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);
     }
   }
 
-  public restartReservationExpiryDateSetInterval(): void {
-    this.stopReservationExpirationSetInterval();
-    this.startReservationExpirationSetInterval();
-  }
-
-  public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean {
-    return this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId)?.idTag === idTag;
-  }
-
-  public isConnectorReservable(
-    reservationId: number,
-    idTag?: string,
-    connectorId?: number,
-  ): boolean {
-    const [alreadyExists] = this.doesReservationExists({ id: reservationId });
-    if (alreadyExists) {
-      return alreadyExists;
+  private stopReservationExpirationSetInterval(): void {
+    if (this.reservationExpirationSetInterval) {
+      clearInterval(this.reservationExpirationSetInterval);
     }
-    const userReservedAlready = isUndefined(
-      this.getReservationBy(ReservationFilterKey.ID_TAG, idTag!),
-    )
-      ? false
-      : true;
-    const notConnectorZero = isUndefined(connectorId) ? true : connectorId! > 0;
-    const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0;
-    return !alreadyExists && !userReservedAlready && notConnectorZero && freeConnectorsAvailable;
   }
 
+  // private restartReservationExpiryDateSetInterval(): void {
+  //   this.stopReservationExpirationSetInterval();
+  //   this.startReservationExpirationSetInterval();
+  // }
+
   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 {
     let numberOfReservations = 0;
-    if (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation) {
-      ++numberOfReservations;
-    } else if (this.connectors.get(0)?.reservation) {
+    if (
+      // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+      (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation) ||
+      (!this.hasEvses && this.connectors.get(0)?.reservation)
+    ) {
       ++numberOfReservations;
     }
     return numberOfReservations;
@@ -1140,12 +1112,6 @@ export class ChargingStation {
     return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
   }
 
-  private stopReservationExpirationSetInterval(): void {
-    if (this.reservationExpirationSetInterval) {
-      clearInterval(this.reservationExpirationSetInterval);
-    }
-  }
-
   private getSupervisionUrlOcppKey(): string {
     return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl;
   }
@@ -1225,7 +1191,7 @@ export class ChargingStation {
       stationTemplate?.firmwareUpgrade ?? {},
     );
     stationInfo.resetTime = !isNullOrUndefined(stationTemplate?.resetTime)
-      ? stationTemplate.resetTime! * 1000
+      ? secondsToMilliseconds(stationTemplate.resetTime!)
       : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
     stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
     return stationInfo;
@@ -1339,7 +1305,7 @@ export class ChargingStation {
     if (this.stationInfo?.autoRegister === true) {
       this.bootNotificationResponse = {
         currentTime: new Date(),
-        interval: this.getHeartbeatInterval() / 1000,
+        interval: millisecondsToSeconds(this.getHeartbeatInterval()),
         status: RegistrationStatusEnumType.ACCEPTED,
       };
     }
@@ -1825,7 +1791,7 @@ export class ChargingStation {
             this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount;
             await sleep(
               this?.bootNotificationResponse?.interval
-                ? this.bootNotificationResponse.interval * 1000
+                ? secondsToMilliseconds(this.bootNotificationResponse.interval)
                 : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL,
             );
           }
@@ -2279,7 +2245,7 @@ export class ChargingStation {
         if (this.isWebSocketConnectionOpened() === true) {
           this.wsConnection?.ping();
         }
-      }, webSocketPingInterval * 1000);
+      }, secondsToMilliseconds(webSocketPingInterval));
       logger.info(
         `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
           webSocketPingInterval,
@@ -2377,7 +2343,7 @@ export class ChargingStation {
       ++this.autoReconnectRetryCount;
       const reconnectDelay = this.getReconnectExponentialDelay()
         ? exponentialDelay(this.autoReconnectRetryCount)
-        : this.getConnectionTimeout() * 1000;
+        : secondsToMilliseconds(this.getConnectionTimeout());
       const reconnectDelayWithdraw = 1000;
       const reconnectTimeout =
         reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0