perf: lookup connector status once at OCPP responses handling
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
index 35da1670922aa1ee60212334e7f90fb562eec9a6..b510051e2085a3ce0d006a1765f05be7603a3318 100644 (file)
@@ -1,9 +1,21 @@
 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
 import type { JSONSchemaType } from 'ajv';
+import {
+  addSeconds,
+  areIntervalsOverlapping,
+  differenceInSeconds,
+  isAfter,
+  isBefore,
+  isWithinInterval,
+} from 'date-fns';
 
 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,
@@ -21,6 +33,7 @@ import {
   type OCPP16ChangeAvailabilityResponse,
   OCPP16ChargePointStatus,
   type OCPP16ChargingProfile,
+  type OCPP16ChargingSchedule,
   type OCPP16IncomingRequestCommand,
   type OCPP16MeterValue,
   OCPP16MeterValueMeasurand,
@@ -838,13 +851,8 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       }
       responses.push(response);
     }
-    if (
-      responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED) &&
-      !responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED)
-    ) {
+    if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) {
       return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
-    } else if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED)) {
-      return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
     }
     return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
   };
@@ -864,7 +872,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
     ) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization`,
+        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type deferred initialization`,
       );
       chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
     }
@@ -891,28 +899,23 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     commandPayload: ClearChargingProfileRequest,
     chargingProfiles: OCPP16ChargingProfile[] | undefined,
   ): boolean => {
+    const { id, chargingProfilePurpose, stackLevel } = commandPayload;
     let clearedCP = false;
     if (isNotEmptyArray(chargingProfiles)) {
       chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
         let clearCurrentCP = false;
-        if (chargingProfile.chargingProfileId === commandPayload.id) {
+        if (chargingProfile.chargingProfileId === id) {
           clearCurrentCP = true;
         }
-        if (
-          !commandPayload.chargingProfilePurpose &&
-          chargingProfile.stackLevel === commandPayload.stackLevel
-        ) {
+        if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) {
           clearCurrentCP = true;
         }
-        if (
-          !chargingProfile.stackLevel &&
-          chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
-        ) {
+        if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) {
           clearCurrentCP = true;
         }
         if (
-          chargingProfile.stackLevel === commandPayload.stackLevel &&
-          chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
+          chargingProfile.stackLevel === stackLevel &&
+          chargingProfile.chargingProfilePurpose === chargingProfilePurpose
         ) {
           clearCurrentCP = true;
         }
@@ -929,6 +932,80 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     return clearedCP;
   };
 
+  public static composeChargingSchedules = (
+    chargingSchedule1: OCPP16ChargingSchedule | undefined,
+    chargingSchedule2: OCPP16ChargingSchedule | undefined,
+    targetInterval: Interval,
+  ): OCPP16ChargingSchedule | undefined => {
+    if (!chargingSchedule1 && !chargingSchedule2) {
+      return undefined;
+    }
+    if (chargingSchedule1 && !chargingSchedule2) {
+      return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1, targetInterval);
+    }
+    if (!chargingSchedule1 && chargingSchedule2) {
+      return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2, targetInterval);
+    }
+    const compositeChargingSchedule1: OCPP16ChargingSchedule | undefined =
+      OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval);
+    const compositeChargingSchedule2: OCPP16ChargingSchedule | undefined =
+      OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval);
+    const compositeChargingScheduleInterval1: Interval = {
+      start: compositeChargingSchedule1!.startSchedule!,
+      end: addSeconds(
+        compositeChargingSchedule1!.startSchedule!,
+        compositeChargingSchedule1!.duration!,
+      ),
+    };
+    const compositeChargingScheduleInterval2: Interval = {
+      start: compositeChargingSchedule2!.startSchedule!,
+      end: addSeconds(
+        compositeChargingSchedule2!.startSchedule!,
+        compositeChargingSchedule2!.duration!,
+      ),
+    };
+    if (
+      !areIntervalsOverlapping(
+        compositeChargingScheduleInterval1,
+        compositeChargingScheduleInterval2,
+      )
+    ) {
+      return {
+        ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval)!,
+        ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval)!,
+      };
+    }
+    // FIXME: Handle overlapping intervals
+  };
+
+  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 &&
+        !hasReservationExpired(connectorReservation) &&
+        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+        connectorReservation?.idTag === idTag) ||
+      (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
+        chargingStationReservation &&
+        !hasReservationExpired(chargingStationReservation) &&
+        chargingStationReservation?.idTag === idTag)
+    ) {
+      logger.debug(
+        `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
+        connectorReservation ?? chargingStationReservation,
+      );
+      return true;
+    }
+    return false;
+  };
+
   public static parseJsonSchemaFile<T extends JsonType>(
     relativePath: string,
     moduleName?: string,
@@ -942,6 +1019,69 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     );
   }
 
+  private static composeChargingSchedule = (
+    chargingSchedule: OCPP16ChargingSchedule,
+    targetInterval: Interval,
+  ): OCPP16ChargingSchedule | undefined => {
+    const chargingScheduleInterval: Interval = {
+      start: chargingSchedule.startSchedule!,
+      end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
+    };
+    if (areIntervalsOverlapping(chargingScheduleInterval, targetInterval)) {
+      chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod);
+      if (isBefore(chargingScheduleInterval.start, targetInterval.start)) {
+        return {
+          ...chargingSchedule,
+          startSchedule: targetInterval.start as Date,
+          duration: differenceInSeconds(chargingScheduleInterval.end, targetInterval.start as Date),
+          chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter(
+            (schedulePeriod, index) => {
+              if (
+                isWithinInterval(
+                  addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
+                  targetInterval,
+                )
+              ) {
+                return true;
+              }
+              if (
+                index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
+                !isWithinInterval(
+                  addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
+                  targetInterval,
+                ) &&
+                isWithinInterval(
+                  addSeconds(
+                    chargingScheduleInterval.start,
+                    chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
+                  ),
+                  targetInterval,
+                )
+              ) {
+                schedulePeriod.startPeriod = 0;
+                return true;
+              }
+              return false;
+            },
+          ),
+        };
+      }
+      if (isAfter(chargingScheduleInterval.end, targetInterval.end)) {
+        return {
+          ...chargingSchedule,
+          duration: differenceInSeconds(targetInterval.end as Date, chargingScheduleInterval.start),
+          chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
+            isWithinInterval(
+              addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
+              targetInterval,
+            ),
+          ),
+        };
+      }
+      return chargingSchedule;
+    }
+  };
+
   private static buildSampledValue(
     sampledValueTemplate: SampledValueTemplate,
     value: number,
@@ -996,25 +1136,25 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     }
   }
 
-  private static getMeasurandDefaultUnit(
-    measurandType: OCPP16MeterValueMeasurand,
-  ): MeterValueUnit | undefined {
-    switch (measurandType) {
-      case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
-      case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
-      case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
-        return MeterValueUnit.AMP;
-      case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
-      case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
-        return MeterValueUnit.WATT_HOUR;
-      case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
-      case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
-      case OCPP16MeterValueMeasurand.POWER_OFFERED:
-        return MeterValueUnit.WATT;
-      case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
-        return MeterValueUnit.PERCENT;
-      case OCPP16MeterValueMeasurand.VOLTAGE:
-        return MeterValueUnit.VOLT;
-    }
-  }
+  // private static getMeasurandDefaultUnit(
+  //   measurandType: OCPP16MeterValueMeasurand,
+  // ): MeterValueUnit | undefined {
+  //   switch (measurandType) {
+  //     case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
+  //     case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
+  //     case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
+  //       return MeterValueUnit.AMP;
+  //     case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
+  //     case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
+  //       return MeterValueUnit.WATT_HOUR;
+  //     case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
+  //     case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
+  //     case OCPP16MeterValueMeasurand.POWER_OFFERED:
+  //       return MeterValueUnit.WATT;
+  //     case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
+  //       return MeterValueUnit.PERCENT;
+  //     case OCPP16MeterValueMeasurand.VOLTAGE:
+  //       return MeterValueUnit.VOLT;
+  //   }
+  // }
 }