fix: avoid gaps in get composite schedule
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16IncomingRequestService.ts
index e3375d192d1615648f4c0910fcc19aa5a7c84f60..d24a2d4cd36e2716ab35b375af7b433017e4a9f3 100644 (file)
@@ -8,10 +8,15 @@ import type { JSONSchemaType } from 'ajv';
 import { Client, type FTPResponse } from 'basic-ftp';
 import {
   addSeconds,
+  differenceInSeconds,
+  isAfter,
+  isBefore,
   isDate,
   isWithinInterval,
+  max,
   maxTime,
   min,
+  minTime,
   secondsToMilliseconds,
 } from 'date-fns';
 import { create } from 'tar';
@@ -21,10 +26,10 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
 import {
   type ChargingStation,
   canProceedChargingProfile,
-  canProceedRecurringChargingProfile,
   checkChargingStation,
   getConfigurationKey,
-  prepareRecurringChargingProfile,
+  getConnectorChargingProfiles,
+  prepareChargingProfileKind,
   removeExpiredReservations,
   setConfigurationKeyValue,
 } from '../../../charging-station';
@@ -32,8 +37,6 @@ import { OCPPError } from '../../../exception';
 import {
   type ChangeConfigurationRequest,
   type ChangeConfigurationResponse,
-  ChargingProfileKindType,
-  ChargingRateUnitType,
   type ClearChargingProfileRequest,
   type ClearChargingProfileResponse,
   ErrorType,
@@ -57,7 +60,9 @@ import {
   OCPP16ChargePointStatus,
   type OCPP16ChargingProfile,
   OCPP16ChargingProfilePurposeType,
+  OCPP16ChargingRateUnitType,
   type OCPP16ChargingSchedule,
+  type OCPP16ChargingSchedulePeriod,
   type OCPP16ClearCacheRequest,
   type OCPP16DataTransferRequest,
   type OCPP16DataTransferResponse,
@@ -101,7 +106,6 @@ import {
 } from '../../../types';
 import {
   Constants,
-  cloneObject,
   convertToDate,
   convertToInt,
   formatDurationMilliSeconds,
@@ -682,12 +686,11 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       return OCPP16Constants.OCPP_RESPONSE_REJECTED;
     }
     if (chargingRateUnit) {
-      logger.error(
-        `${chargingStation.logPrefix()} Get composite schedule with a specified rate unit is not yet supported`,
+      logger.warn(
+        `${chargingStation.logPrefix()} Get composite schedule with a specified rate unit is not yet supported, no conversion will be done`,
       );
-      return OCPP16Constants.OCPP_RESPONSE_REJECTED;
     }
-    const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
     if (
       isEmptyArray(
         connectorStatus?.chargingProfiles &&
@@ -701,53 +704,63 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       start: currentDate,
       end: addSeconds(currentDate, duration),
     };
+    // Get charging profiles sorted by connector id then stack level
+    const storedChargingProfiles: OCPP16ChargingProfile[] = getConnectorChargingProfiles(
+      chargingStation,
+      connectorId,
+    );
     const chargingProfiles: OCPP16ChargingProfile[] = [];
-    for (const chargingProfile of cloneObject<OCPP16ChargingProfile[]>(
-      (connectorStatus?.chargingProfiles ?? []).concat(
-        chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [],
-      ),
-    ).sort((a, b) => b.stackLevel - a.stackLevel)) {
+    for (const storedChargingProfile of storedChargingProfiles) {
       if (
-        connectorStatus?.transactionStarted &&
-        isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule)
+        isNullOrUndefined(storedChargingProfile.chargingSchedule?.startSchedule) &&
+        connectorStatus?.transactionStarted
       ) {
         logger.debug(
           `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
-            chargingProfile.chargingProfileId
+            storedChargingProfile.chargingProfileId
           } has no startSchedule defined. Trying to set it to the connector current transaction start date`,
         );
         // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
-        chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
+        storedChargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
+      }
+      if (
+        !isNullOrUndefined(storedChargingProfile.chargingSchedule?.startSchedule) &&
+        isNullOrUndefined(storedChargingProfile.chargingSchedule?.duration)
+      ) {
+        logger.debug(
+          `${chargingStation.logPrefix()} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${
+            storedChargingProfile.chargingProfileId
+          } has no duration defined and will be set to the maximum time allowed`,
+        );
+        // OCPP specifies that if duration is not defined, it should be infinite
+        storedChargingProfile.chargingSchedule.duration = differenceInSeconds(
+          maxTime,
+          storedChargingProfile.chargingSchedule.startSchedule!,
+        );
       }
-      if (!isDate(chargingProfile.chargingSchedule?.startSchedule)) {
+      if (!isDate(storedChargingProfile.chargingSchedule?.startSchedule)) {
         logger.warn(
           `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
-            chargingProfile.chargingProfileId
+            storedChargingProfile.chargingProfileId
           } startSchedule property is not a Date object. Trying to convert it to a Date object`,
         );
-        chargingProfile.chargingSchedule.startSchedule = convertToDate(
-          chargingProfile.chargingSchedule?.startSchedule,
+        storedChargingProfile.chargingSchedule.startSchedule = convertToDate(
+          storedChargingProfile.chargingSchedule?.startSchedule,
         )!;
       }
-      switch (chargingProfile.chargingProfileKind) {
-        case ChargingProfileKindType.RECURRING:
-          if (!canProceedRecurringChargingProfile(chargingProfile, chargingStation.logPrefix())) {
-            continue;
-          }
-          prepareRecurringChargingProfile(
-            chargingProfile,
-            interval.start as Date,
-            chargingStation.logPrefix(),
-          );
-          break;
-        case ChargingProfileKindType.RELATIVE:
-          connectorStatus?.transactionStarted &&
-            (chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart);
-          break;
+      if (
+        !prepareChargingProfileKind(
+          connectorStatus,
+          storedChargingProfile,
+          interval.start as Date,
+          chargingStation.logPrefix(),
+        )
+      ) {
+        continue;
       }
       if (
         !canProceedChargingProfile(
-          chargingProfile,
+          storedChargingProfile,
           interval.start as Date,
           chargingStation.logPrefix(),
         )
@@ -756,45 +769,181 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       }
       // Add active charging profiles into chargingProfiles array
       if (
-        isValidTime(chargingProfile.chargingSchedule?.startSchedule) &&
-        isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, interval)
+        isValidTime(storedChargingProfile.chargingSchedule?.startSchedule) &&
+        isWithinInterval(storedChargingProfile.chargingSchedule.startSchedule!, interval)
       ) {
-        chargingProfiles.push(chargingProfile);
+        if (isEmptyArray(chargingProfiles)) {
+          if (
+            isAfter(
+              addSeconds(
+                storedChargingProfile.chargingSchedule.startSchedule!,
+                storedChargingProfile.chargingSchedule.duration!,
+              ),
+              interval.end,
+            )
+          ) {
+            storedChargingProfile.chargingSchedule.chargingSchedulePeriod =
+              storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter(
+                (schedulePeriod) =>
+                  isWithinInterval(
+                    addSeconds(
+                      storedChargingProfile.chargingSchedule.startSchedule!,
+                      schedulePeriod.startPeriod,
+                    )!,
+                    interval,
+                  ),
+              );
+            storedChargingProfile.chargingSchedule.duration = differenceInSeconds(
+              interval.end,
+              storedChargingProfile.chargingSchedule.startSchedule!,
+            );
+          }
+          chargingProfiles.push(storedChargingProfile);
+        } else if (isNotEmptyArray(chargingProfiles)) {
+          const chargingProfilesInterval: Interval = {
+            start: min(
+              chargingProfiles.map(
+                (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime,
+              ),
+            ),
+            end: max(
+              chargingProfiles.map(
+                (chargingProfile) =>
+                  addSeconds(
+                    chargingProfile.chargingSchedule.startSchedule!,
+                    chargingProfile.chargingSchedule.duration!,
+                  ) ?? minTime,
+              ),
+            ),
+          };
+          let addChargingProfile = false;
+          if (
+            isBefore(interval.start, chargingProfilesInterval.start) &&
+            isBefore(
+              storedChargingProfile.chargingSchedule.startSchedule!,
+              chargingProfilesInterval.start,
+            )
+          ) {
+            // Remove charging schedule periods that are after the start of the active profiles interval
+            storedChargingProfile.chargingSchedule.chargingSchedulePeriod =
+              storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter(
+                (schedulePeriod) =>
+                  isWithinInterval(
+                    addSeconds(
+                      storedChargingProfile.chargingSchedule.startSchedule!,
+                      schedulePeriod.startPeriod,
+                    ),
+                    {
+                      start: interval.start,
+                      end: chargingProfilesInterval.start,
+                    },
+                  ),
+              );
+            addChargingProfile = true;
+          }
+          if (
+            isBefore(chargingProfilesInterval.end, interval.end) &&
+            isAfter(
+              addSeconds(
+                storedChargingProfile.chargingSchedule.startSchedule!,
+                storedChargingProfile.chargingSchedule.duration!,
+              ),
+              chargingProfilesInterval.end,
+            )
+          ) {
+            // Remove charging schedule periods that are before the end of the active profiles interval
+            storedChargingProfile.chargingSchedule.chargingSchedulePeriod =
+              storedChargingProfile.chargingSchedule.chargingSchedulePeriod.filter(
+                (schedulePeriod, index) => {
+                  if (
+                    isWithinInterval(
+                      addSeconds(
+                        storedChargingProfile.chargingSchedule.startSchedule!,
+                        schedulePeriod.startPeriod,
+                      ),
+                      {
+                        start: chargingProfilesInterval.end,
+                        end: interval.end,
+                      },
+                    )
+                  ) {
+                    return true;
+                  }
+                  if (
+                    !isWithinInterval(
+                      addSeconds(
+                        storedChargingProfile.chargingSchedule.startSchedule!,
+                        schedulePeriod.startPeriod,
+                      ),
+                      {
+                        start: chargingProfilesInterval.end,
+                        end: interval.end,
+                      },
+                    ) &&
+                    index <
+                      storedChargingProfile.chargingSchedule.chargingSchedulePeriod.length - 1 &&
+                    isWithinInterval(
+                      addSeconds(
+                        storedChargingProfile.chargingSchedule.startSchedule!,
+                        storedChargingProfile.chargingSchedule.chargingSchedulePeriod[index + 1]
+                          .startPeriod,
+                      ),
+                      {
+                        start: chargingProfilesInterval.end,
+                        end: interval.end,
+                      },
+                    )
+                  ) {
+                    return true;
+                  }
+                  return false;
+                },
+              );
+            addChargingProfile = true;
+          }
+          addChargingProfile && chargingProfiles.push(storedChargingProfile);
+        }
       }
     }
-    const compositeSchedule: OCPP16ChargingSchedule = {
-      startSchedule: min(
-        chargingProfiles.map(
-          (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime,
-        ),
+    const compositeScheduleStart: Date = min(
+      chargingProfiles.map(
+        (chargingProfile) => chargingProfile.chargingSchedule.startSchedule ?? maxTime,
       ),
-      duration: Math.max(
-        ...chargingProfiles.map(
-          (chargingProfile) => chargingProfile.chargingSchedule.duration ?? -Infinity,
-        ),
+    );
+    const compositeScheduleDuration: number = Math.max(
+      ...chargingProfiles.map((chargingProfile) =>
+        isNaN(chargingProfile.chargingSchedule.duration!)
+          ? -Infinity
+          : chargingProfile.chargingSchedule.duration!,
       ),
+    );
+    const compositeSchedulePeriods: OCPP16ChargingSchedulePeriod[] = chargingProfiles
+      .map((chargingProfile) => chargingProfile.chargingSchedule.chargingSchedulePeriod)
+      .reduce(
+        (accumulator, value) =>
+          accumulator.concat(value).sort((a, b) => a.startPeriod - b.startPeriod),
+        [],
+      );
+    const compositeSchedule: OCPP16ChargingSchedule = {
+      startSchedule: compositeScheduleStart,
+      duration: compositeScheduleDuration,
       chargingRateUnit: chargingProfiles.every(
         (chargingProfile) =>
-          chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.AMPERE,
+          chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.AMPERE,
       )
-        ? ChargingRateUnitType.AMPERE
+        ? OCPP16ChargingRateUnitType.AMPERE
         : chargingProfiles.every(
             (chargingProfile) =>
-              chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT,
+              chargingProfile.chargingSchedule.chargingRateUnit === OCPP16ChargingRateUnitType.WATT,
           )
-        ? ChargingRateUnitType.WATT
-        : ChargingRateUnitType.AMPERE,
-      // FIXME: remove overlapping charging schedule periods
-      chargingSchedulePeriod: chargingProfiles
-        .map((chargingProfile) => chargingProfile.chargingSchedule.chargingSchedulePeriod)
-        .reduce(
-          (accumulator, value) =>
-            accumulator.concat(value).sort((a, b) => a.startPeriod - b.startPeriod),
-          [],
-        ),
+        ? OCPP16ChargingRateUnitType.WATT
+        : OCPP16ChargingRateUnitType.AMPERE,
+      chargingSchedulePeriod: compositeSchedulePeriods,
       minChargeRate: Math.min(
-        ...chargingProfiles.map(
-          (chargingProfile) => chargingProfile.chargingSchedule.minChargeRate ?? Infinity,
+        ...chargingProfiles.map((chargingProfile) =>
+          isNaN(chargingProfile.chargingSchedule.minChargeRate!)
+            ? Infinity
+            : chargingProfile.chargingSchedule.minChargeRate!,
         ),
       ),
     };