Merge dependabot/github_actions/docker/setup-buildx-action-3 into combined-prs-branch
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
index 502258c2552c670a5ef0f498a1858118e4f9372c..7ccd3dcd00b74967b5e6f6ee8d776782df1a405a 100644 (file)
@@ -1,6 +1,14 @@
 // 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 {
@@ -25,6 +33,7 @@ import {
   type OCPP16ChangeAvailabilityResponse,
   OCPP16ChargePointStatus,
   type OCPP16ChargingProfile,
+  type OCPP16ChargingSchedule,
   type OCPP16IncomingRequestCommand,
   type OCPP16MeterValue,
   OCPP16MeterValueMeasurand,
@@ -923,6 +932,198 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     return clearedCP;
   };
 
+  public static composeChargingSchedules = (
+    chargingScheduleHigher: OCPP16ChargingSchedule | undefined,
+    chargingScheduleLower: OCPP16ChargingSchedule | undefined,
+    compositeInterval: Interval,
+  ): OCPP16ChargingSchedule | undefined => {
+    if (!chargingScheduleHigher && !chargingScheduleLower) {
+      return undefined;
+    }
+    if (chargingScheduleHigher && !chargingScheduleLower) {
+      return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, compositeInterval);
+    }
+    if (!chargingScheduleHigher && chargingScheduleLower) {
+      return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, compositeInterval);
+    }
+    const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined =
+      OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, compositeInterval);
+    const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined =
+      OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval);
+    const compositeChargingScheduleHigherInterval: Interval = {
+      start: compositeChargingScheduleHigher!.startSchedule!,
+      end: addSeconds(
+        compositeChargingScheduleHigher!.startSchedule!,
+        compositeChargingScheduleHigher!.duration!,
+      ),
+    };
+    const compositeChargingScheduleLowerInterval: Interval = {
+      start: compositeChargingScheduleLower!.startSchedule!,
+      end: addSeconds(
+        compositeChargingScheduleLower!.startSchedule!,
+        compositeChargingScheduleLower!.duration!,
+      ),
+    };
+    const higherFirst = isBefore(
+      compositeChargingScheduleHigherInterval.start,
+      compositeChargingScheduleLowerInterval.start,
+    );
+    if (
+      !areIntervalsOverlapping(
+        compositeChargingScheduleHigherInterval,
+        compositeChargingScheduleLowerInterval,
+      )
+    ) {
+      return {
+        ...compositeChargingScheduleLower,
+        ...compositeChargingScheduleHigher!,
+        startSchedule: higherFirst
+          ? (compositeChargingScheduleHigherInterval.start as Date)
+          : (compositeChargingScheduleLowerInterval.start as Date),
+        duration: higherFirst
+          ? differenceInSeconds(
+              compositeChargingScheduleLowerInterval.end,
+              compositeChargingScheduleHigherInterval.start,
+            )
+          : differenceInSeconds(
+              compositeChargingScheduleHigherInterval.end,
+              compositeChargingScheduleLowerInterval.start,
+            ),
+        chargingSchedulePeriod: [
+          ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
+            return {
+              ...schedulePeriod,
+              startPeriod: higherFirst
+                ? 0
+                : schedulePeriod.startPeriod +
+                  differenceInSeconds(
+                    compositeChargingScheduleHigherInterval.start,
+                    compositeChargingScheduleLowerInterval.start,
+                  ),
+            };
+          }),
+          ...compositeChargingScheduleLower!.chargingSchedulePeriod.map((schedulePeriod) => {
+            return {
+              ...schedulePeriod,
+              startPeriod: higherFirst
+                ? schedulePeriod.startPeriod +
+                  differenceInSeconds(
+                    compositeChargingScheduleLowerInterval.start,
+                    compositeChargingScheduleHigherInterval.start,
+                  )
+                : 0,
+            };
+          }),
+        ].sort((a, b) => a.startPeriod - b.startPeriod),
+      };
+    }
+    return {
+      ...compositeChargingScheduleLower,
+      ...compositeChargingScheduleHigher!,
+      startSchedule: higherFirst
+        ? (compositeChargingScheduleHigherInterval.start as Date)
+        : (compositeChargingScheduleLowerInterval.start as Date),
+      duration: higherFirst
+        ? differenceInSeconds(
+            compositeChargingScheduleLowerInterval.end,
+            compositeChargingScheduleHigherInterval.start,
+          )
+        : differenceInSeconds(
+            compositeChargingScheduleHigherInterval.end,
+            compositeChargingScheduleLowerInterval.start,
+          ),
+      chargingSchedulePeriod: [
+        ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
+          return {
+            ...schedulePeriod,
+            startPeriod: higherFirst
+              ? 0
+              : schedulePeriod.startPeriod +
+                differenceInSeconds(
+                  compositeChargingScheduleHigherInterval.start,
+                  compositeChargingScheduleLowerInterval.start,
+                ),
+          };
+        }),
+        ...compositeChargingScheduleLower!.chargingSchedulePeriod
+          .filter((schedulePeriod, index) => {
+            if (
+              higherFirst &&
+              isWithinInterval(
+                addSeconds(
+                  compositeChargingScheduleLowerInterval.start,
+                  schedulePeriod.startPeriod,
+                ),
+                {
+                  start: compositeChargingScheduleLowerInterval.start,
+                  end: compositeChargingScheduleHigherInterval.end,
+                },
+              )
+            ) {
+              return false;
+            }
+            if (
+              higherFirst &&
+              index < compositeChargingScheduleLower!.chargingSchedulePeriod.length - 1 &&
+              !isWithinInterval(
+                addSeconds(
+                  compositeChargingScheduleLowerInterval.start,
+                  schedulePeriod.startPeriod,
+                ),
+                {
+                  start: compositeChargingScheduleLowerInterval.start,
+                  end: compositeChargingScheduleHigherInterval.end,
+                },
+              ) &&
+              isWithinInterval(
+                addSeconds(
+                  compositeChargingScheduleLowerInterval.start,
+                  compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod,
+                ),
+                {
+                  start: compositeChargingScheduleLowerInterval.start,
+                  end: compositeChargingScheduleHigherInterval.end,
+                },
+              )
+            ) {
+              return false;
+            }
+            if (
+              !higherFirst &&
+              isWithinInterval(
+                addSeconds(
+                  compositeChargingScheduleLowerInterval.start,
+                  schedulePeriod.startPeriod,
+                ),
+                {
+                  start: compositeChargingScheduleHigherInterval.start,
+                  end: compositeChargingScheduleLowerInterval.end,
+                },
+              )
+            ) {
+              return false;
+            }
+            return true;
+          })
+          .map((schedulePeriod, index) => {
+            if (index === 0 && schedulePeriod.startPeriod !== 0) {
+              schedulePeriod.startPeriod = 0;
+            }
+            return {
+              ...schedulePeriod,
+              startPeriod: higherFirst
+                ? schedulePeriod.startPeriod +
+                  differenceInSeconds(
+                    compositeChargingScheduleLowerInterval.start,
+                    compositeChargingScheduleHigherInterval.start,
+                  )
+                : 0,
+            };
+          }),
+      ].sort((a, b) => a.startPeriod - b.startPeriod),
+    };
+  };
+
   public static hasReservation = (
     chargingStation: ChargingStation,
     connectorId: number,
@@ -964,6 +1165,79 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     );
   }
 
+  private static composeChargingSchedule = (
+    chargingSchedule: OCPP16ChargingSchedule,
+    compositeInterval: Interval,
+  ): OCPP16ChargingSchedule | undefined => {
+    const chargingScheduleInterval: Interval = {
+      start: chargingSchedule.startSchedule!,
+      end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
+    };
+    if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) {
+      chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod);
+      if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) {
+        return {
+          ...chargingSchedule,
+          startSchedule: compositeInterval.start as Date,
+          duration: differenceInSeconds(
+            chargingScheduleInterval.end,
+            compositeInterval.start as Date,
+          ),
+          chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod
+            .filter((schedulePeriod, index) => {
+              if (
+                isWithinInterval(
+                  addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
+                  compositeInterval,
+                )
+              ) {
+                return true;
+              }
+              if (
+                index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
+                !isWithinInterval(
+                  addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
+                  compositeInterval,
+                ) &&
+                isWithinInterval(
+                  addSeconds(
+                    chargingScheduleInterval.start,
+                    chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
+                  ),
+                  compositeInterval,
+                )
+              ) {
+                return true;
+              }
+              return false;
+            })
+            .map((schedulePeriod, index) => {
+              if (index === 0 && schedulePeriod.startPeriod !== 0) {
+                schedulePeriod.startPeriod = 0;
+              }
+              return schedulePeriod;
+            }),
+        };
+      }
+      if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) {
+        return {
+          ...chargingSchedule,
+          duration: differenceInSeconds(
+            compositeInterval.end as Date,
+            chargingScheduleInterval.start,
+          ),
+          chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
+            isWithinInterval(
+              addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
+              compositeInterval,
+            ),
+          ),
+        };
+      }
+      return chargingSchedule;
+    }
+  };
+
   private static buildSampledValue(
     sampledValueTemplate: SampledValueTemplate,
     value: number,