refactor: add more sanity checks to charging profiles handling code
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
index 9da406c282d448b8b713dd116658433c4e291994..628b7eb9c03510511c6d48736726186daf028534 100644 (file)
@@ -9,6 +9,7 @@ import {
   addSeconds,
   addWeeks,
   differenceInDays,
+  differenceInSeconds,
   differenceInWeeks,
   isAfter,
   isBefore,
@@ -48,6 +49,7 @@ import {
   cloneObject,
   convertToDate,
   convertToInt,
+  isArraySorted,
   isEmptyObject,
   isEmptyString,
   isNotEmptyArray,
@@ -473,7 +475,7 @@ export const getChargingStationConnectorChargingProfilesPowerLimit = (
       chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
     )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
   // Get charging profiles on connector 0 and sort by stack level
-  if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
+  if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
     chargingProfiles.push(
       ...cloneObject<ChargingProfile[]>(
         chargingStation.getConnectorStatus(0)!.chargingProfiles!,
@@ -513,7 +515,7 @@ export const getChargingStationConnectorChargingProfilesPowerLimit = (
         chargingStation.getMaximumPower() / chargingStation.powerDivider;
       if (limit! > connectorMaximumPower) {
         logger.error(
-          `${chargingStation.logPrefix()} Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
+          `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
           result,
         );
         limit = connectorMaximumPower;
@@ -670,8 +672,10 @@ interface ChargingProfilesLimit {
 }
 
 /**
- * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
+ * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
  *
+ * @param chargingStation -
+ * @param connectorId -
  * @param chargingProfiles -
  * @param logPrefix -
  * @returns ChargingProfilesLimit
@@ -687,12 +691,9 @@ const getLimitFromChargingProfiles = (
   const connectorStatus = chargingStation.getConnectorStatus(connectorId);
   for (const chargingProfile of chargingProfiles) {
     if (
-      isValidDate(chargingProfile.validFrom) &&
-      isValidDate(chargingProfile.validTo) &&
-      !isWithinInterval(currentDate, {
-        start: chargingProfile.validFrom!,
-        end: chargingProfile.validTo!,
-      })
+      (isValidDate(chargingProfile.validFrom) &&
+        isBefore(currentDate, chargingProfile.validFrom!)) ||
+      (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
     ) {
       logger.debug(
         `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${
@@ -704,16 +705,16 @@ const getLimitFromChargingProfiles = (
     const chargingSchedule = chargingProfile.chargingSchedule;
     if (connectorStatus?.transactionStarted && !chargingSchedule?.startSchedule) {
       logger.debug(
-        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}. Trying to set it to the connector transaction start date`,
+        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.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
       chargingSchedule.startSchedule = connectorStatus?.transactionStart;
     }
     if (!(chargingSchedule?.startSchedule instanceof Date)) {
       logger.warn(
-        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`,
+        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
       );
-      chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!;
+      chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
     }
     if (
       chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
@@ -732,17 +733,51 @@ const getLimitFromChargingProfiles = (
     ) {
       chargingSchedule.startSchedule = connectorStatus?.transactionStart;
     }
+    if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
+      logger.error(
+        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
+      );
+      continue;
+    }
+    if (isNullOrUndefined(chargingSchedule?.duration)) {
+      logger.error(
+        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
+      );
+      continue;
+    }
     // Check if the charging profile is active
     if (
-      isValidDate(chargingSchedule.startSchedule) &&
-      isAfter(addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!), currentDate)
+      isValidDate(chargingSchedule?.startSchedule) &&
+      isWithinInterval(currentDate, {
+        start: chargingSchedule.startSchedule!,
+        end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
+      })
     ) {
       if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
-        // Handling of only one schedule period
+        const chargingSchedulePeriodCompareFn = (
+          a: ChargingSchedulePeriod,
+          b: ChargingSchedulePeriod,
+        ) => a.startPeriod - b.startPeriod;
         if (
-          chargingSchedule.chargingSchedulePeriod.length === 1 &&
-          chargingSchedule.chargingSchedulePeriod[0].startPeriod === 0
+          isArraySorted<ChargingSchedulePeriod>(
+            chargingSchedule.chargingSchedulePeriod,
+            chargingSchedulePeriodCompareFn,
+          ) === false
         ) {
+          logger.warn(
+            `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
+          );
+          chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
+        }
+        // Check if the first schedule period start period is equal to 0
+        if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
+          logger.error(
+            `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
+          );
+          continue;
+        }
+        // Handle only one schedule period
+        if (chargingSchedule.chargingSchedulePeriod.length === 1) {
           const result: ChargingProfilesLimit = {
             limit: chargingSchedule.chargingSchedulePeriod[0].limit,
             matchingChargingProfile: chargingProfile,
@@ -752,7 +787,7 @@ const getLimitFromChargingProfiles = (
         }
         let lastButOneSchedule: ChargingSchedulePeriod | undefined;
         // Search for the right schedule period
-        for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
+        for (const [index, schedulePeriod] of chargingSchedule.chargingSchedulePeriod.entries()) {
           // Find the right schedule period
           if (
             isAfter(
@@ -770,12 +805,18 @@ const getLimitFromChargingProfiles = (
           }
           // Keep it
           lastButOneSchedule = schedulePeriod;
-          // Handle the last schedule period
+          // Handle the last schedule period within the charging profile duration
           if (
-            schedulePeriod.startPeriod ===
-            chargingSchedule.chargingSchedulePeriod[
-              chargingSchedule.chargingSchedulePeriod.length - 1
-            ].startPeriod
+            index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
+            (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
+              chargingSchedule.duration! >
+                differenceInSeconds(
+                  addSeconds(
+                    chargingSchedule.startSchedule!,
+                    chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
+                  ),
+                  chargingSchedule.startSchedule!,
+                ))
           ) {
             const result: ChargingProfilesLimit = {
               limit: lastButOneSchedule.limit,
@@ -791,7 +832,7 @@ const getLimitFromChargingProfiles = (
 };
 
 /**
- *  Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
+ * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
  *
  * @param chargingProfile -
  * @param currentDate -
@@ -801,8 +842,9 @@ const prepareRecurringChargingProfile = (
   chargingProfile: ChargingProfile,
   currentDate: Date,
   logPrefix: string,
-) => {
+): boolean => {
   const chargingSchedule = chargingProfile.chargingSchedule;
+  let recurringIntervalTranslated = false;
   let recurringInterval: Interval;
   switch (chargingProfile.recurrencyKind) {
     case RecurrencyKindType.DAILY:
@@ -810,18 +852,20 @@ const prepareRecurringChargingProfile = (
         start: chargingSchedule.startSchedule!,
         end: addDays(chargingSchedule.startSchedule!, 1),
       };
+      checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
       if (
         !isWithinInterval(currentDate, recurringInterval) &&
-        isBefore(chargingSchedule.startSchedule!, currentDate)
+        isBefore(recurringInterval.end, currentDate)
       ) {
         chargingSchedule.startSchedule = addDays(
-          chargingSchedule.startSchedule!,
-          differenceInDays(chargingSchedule.startSchedule!, recurringInterval.end),
+          recurringInterval.start,
+          differenceInDays(currentDate, recurringInterval.start),
         );
         recurringInterval = {
           start: chargingSchedule.startSchedule,
           end: addDays(chargingSchedule.startSchedule, 1),
         };
+        recurringIntervalTranslated = true;
       }
       break;
     case RecurrencyKindType.WEEKLY:
@@ -829,31 +873,72 @@ const prepareRecurringChargingProfile = (
         start: chargingSchedule.startSchedule!,
         end: addWeeks(chargingSchedule.startSchedule!, 1),
       };
+      checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
       if (
         !isWithinInterval(currentDate, recurringInterval) &&
-        isBefore(chargingSchedule.startSchedule!, currentDate)
+        isBefore(recurringInterval.end, currentDate)
       ) {
         chargingSchedule.startSchedule = addWeeks(
-          chargingSchedule.startSchedule!,
-          differenceInWeeks(chargingSchedule.startSchedule!, recurringInterval.end),
+          recurringInterval.start,
+          differenceInWeeks(currentDate, recurringInterval.start),
         );
         recurringInterval = {
           start: chargingSchedule.startSchedule,
           end: addWeeks(chargingSchedule.startSchedule, 1),
         };
+        recurringIntervalTranslated = true;
       }
       break;
+    default:
+      logger.error(
+        `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
+      );
   }
-  if (!isWithinInterval(currentDate, recurringInterval!)) {
+  if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
     logger.error(
-      `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring ${
+      `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
         chargingProfile.recurrencyKind
+      } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
+        recurringInterval!.start,
+      ).toISOString()}, ${toDate(
+        recurringInterval!.end,
+      ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
+    );
+  }
+  return recurringIntervalTranslated;
+};
+
+const checkRecurringChargingProfileDuration = (
+  chargingProfile: ChargingProfile,
+  interval: Interval,
+  logPrefix: string,
+): void => {
+  if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
+    logger.warn(
+      `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
+        chargingProfile.chargingProfileKind
       } charging profile id ${
         chargingProfile.chargingProfileId
-      } startSchedule ${chargingSchedule.startSchedule!.toISOString()} is not properly translated to current recurrency time interval [${toDate(
-        recurringInterval!.start,
-      ).toISOString()}, ${toDate(recurringInterval!.end).toISOString()}]`,
+      } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
+        interval.end,
+        interval.start,
+      )}`,
+    );
+    chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
+  } else if (
+    chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
+  ) {
+    logger.warn(
+      `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
+        chargingProfile.chargingProfileKind
+      } charging profile id ${chargingProfile.chargingProfileId} duration ${
+        chargingProfile.chargingSchedule.duration
+      } is greater than the recurrency time interval duration ${differenceInSeconds(
+        interval.end,
+        interval.start,
+      )}`,
     );
+    chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
   }
 };