refactor: improve tx changing profile checks
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
index bf019cee3c6fffc6756f0f8d0a97b664dc676387..b510051e2085a3ce0d006a1765f05be7603a3318 100644 (file)
@@ -1,14 +1,27 @@
 // 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 { type ChargingStation, getIdTagsFile, hasFeatureProfile } from '../../../charging-station';
+import { OCPP16Constants } from './OCPP16Constants';
+import {
+  type ChargingStation,
+  hasFeatureProfile,
+  hasReservationExpired,
+} from '../../../charging-station';
 import { OCPPError } from '../../../exception';
 import {
   type ClearChargingProfileRequest,
-  type ConnectorStatus,
   CurrentType,
   ErrorType,
+  type GenericResponse,
   type JsonType,
   type MeasurandPerPhaseSampledValueTemplates,
   type MeasurandValues,
@@ -16,9 +29,11 @@ import {
   MeterValueLocation,
   MeterValueUnit,
   OCPP16AuthorizationStatus,
-  type OCPP16AuthorizeRequest,
-  type OCPP16AuthorizeResponse,
+  OCPP16AvailabilityType,
+  type OCPP16ChangeAvailabilityResponse,
+  OCPP16ChargePointStatus,
   type OCPP16ChargingProfile,
+  type OCPP16ChargingSchedule,
   type OCPP16IncomingRequestCommand,
   type OCPP16MeterValue,
   OCPP16MeterValueMeasurand,
@@ -26,6 +41,7 @@ import {
   OCPP16RequestCommand,
   type OCPP16SampledValue,
   OCPP16StandardParametersKey,
+  OCPP16StopTransactionReason,
   type OCPP16SupportedFeatureProfiles,
   OCPPVersion,
   type SampledValueTemplate,
@@ -41,7 +57,6 @@ import {
   getRandomFloatRounded,
   getRandomInteger,
   isNotEmptyArray,
-  isNotEmptyString,
   isNullOrUndefined,
   isUndefined,
   logger,
@@ -793,6 +808,55 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     return meterValues;
   }
 
+  public static remoteStopTransaction = async (
+    chargingStation: ChargingStation,
+    connectorId: number,
+  ): Promise<GenericResponse> => {
+    await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+      chargingStation,
+      connectorId,
+      OCPP16ChargePointStatus.Finishing,
+    );
+    const stopResponse = await chargingStation.stopTransactionOnConnector(
+      connectorId,
+      OCPP16StopTransactionReason.REMOTE,
+    );
+    if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
+      return OCPP16Constants.OCPP_RESPONSE_ACCEPTED;
+    }
+    return OCPP16Constants.OCPP_RESPONSE_REJECTED;
+  };
+
+  public static changeAvailability = async (
+    chargingStation: ChargingStation,
+    connectorIds: number[],
+    chargePointStatus: OCPP16ChargePointStatus,
+    availabilityType: OCPP16AvailabilityType,
+  ): Promise<OCPP16ChangeAvailabilityResponse> => {
+    const responses: OCPP16ChangeAvailabilityResponse[] = [];
+    for (const connectorId of connectorIds) {
+      let response: OCPP16ChangeAvailabilityResponse =
+        OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
+      const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
+      if (connectorStatus?.transactionStarted === true) {
+        response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
+      }
+      connectorStatus.availability = availabilityType;
+      if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
+        await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+          chargingStation,
+          connectorId,
+          chargePointStatus,
+        );
+      }
+      responses.push(response);
+    }
+    if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) {
+      return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
+    }
+    return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
+  };
+
   public static setChargingProfile(
     chargingStation: ChargingStation,
     connectorId: number,
@@ -808,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 = [];
     }
@@ -835,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;
         }
@@ -873,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,
@@ -886,28 +1019,68 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     );
   }
 
-  public static async isIdTagAuthorized(
-    chargingStation: ChargingStation,
-    connectorId: number,
-    idTag: string,
-  ): Promise<boolean> {
-    let authorized = false;
-    const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!;
-    if (OCPP16ServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)) {
-      connectorStatus.localAuthorizeIdTag = idTag;
-      connectorStatus.idTagLocalAuthorized = true;
-      authorized = true;
-    } else {
-      authorized = await OCPP16ServiceUtils.isIdTagRemoteAuthorized(chargingStation, idTag);
-      if (authorized && isNullOrUndefined(connectorStatus.authorizeIdTag)) {
-        logger.warn(
-          `${chargingStation.logPrefix()} IdTag ${idTag} is not set as authorized remotely, applying deferred initialization`,
-        );
-        connectorStatus.authorizeIdTag = idTag;
+  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;
     }
-    return authorized;
-  }
+  };
 
   private static buildSampledValue(
     sampledValueTemplate: SampledValueTemplate,
@@ -963,51 +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 isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
-    return (
-      chargingStation.getLocalAuthListEnabled() === true &&
-      chargingStation.hasIdTags() === true &&
-      isNotEmptyString(
-        chargingStation.idTagsCache
-          .getIdTags(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,
-      });
-    return authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED;
-  }
+  // 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;
+  //   }
+  // }
 }