fix: warn abount unsupported charging profiles structure
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
index 090ca65d1edcec3648c4fe9f6b3502d3e9097ae6..0eeb205189456c4d0ea44ec96d3cfdf434722075 100644 (file)
@@ -8,14 +8,13 @@ import {
   addDays,
   addSeconds,
   addWeeks,
-  endOfWeek,
+  differenceInDays,
+  differenceInSeconds,
+  differenceInWeeks,
   isAfter,
   isBefore,
-  isTomorrow,
-  isYesterday,
-  startOfWeek,
-  subDays,
-  subWeeks,
+  isWithinInterval,
+  toDate,
 } from 'date-fns';
 
 import type { ChargingStation } from './ChargingStation';
@@ -50,12 +49,14 @@ import {
   cloneObject,
   convertToDate,
   convertToInt,
+  isArraySorted,
   isEmptyObject,
   isEmptyString,
   isNotEmptyArray,
   isNotEmptyString,
   isNullOrUndefined,
   isUndefined,
+  isValidDate,
   logger,
   secureRandom,
 } from '../utils';
@@ -302,9 +303,10 @@ export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void =>
   connectorStatus.idTagAuthorized = false;
   connectorStatus.transactionRemoteStarted = false;
   connectorStatus.transactionStarted = false;
+  delete connectorStatus?.transactionStart;
+  delete connectorStatus?.transactionId;
   delete connectorStatus?.localAuthorizeIdTag;
   delete connectorStatus?.authorizeIdTag;
-  delete connectorStatus?.transactionId;
   delete connectorStatus?.transactionIdTag;
   connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
   delete connectorStatus?.transactionBeginMeterValue;
@@ -467,13 +469,13 @@ export const getChargingStationConnectorChargingProfilesPowerLimit = (
   connectorId: number,
 ): number | undefined => {
   let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
-  // Get charging profiles for connector and sort by stack level
+  // Get charging profiles for connector id and sort by stack level
   const chargingProfiles =
     cloneObject<ChargingProfile[]>(
       chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
     )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
-  // Get profiles on connector 0
-  if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
+  // Get charging profiles on connector 0 and sort by stack level
+  if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
     chargingProfiles.push(
       ...cloneObject<ChargingProfile[]>(
         chargingStation.getConnectorStatus(0)!.chargingProfiles!,
@@ -481,7 +483,12 @@ export const getChargingStationConnectorChargingProfilesPowerLimit = (
     );
   }
   if (isNotEmptyArray(chargingProfiles)) {
-    const result = getLimitFromChargingProfiles(chargingProfiles, chargingStation.logPrefix());
+    const result = getLimitFromChargingProfiles(
+      chargingStation,
+      connectorId,
+      chargingProfiles,
+      chargingStation.logPrefix(),
+    );
     if (!isNullOrUndefined(result)) {
       limit = result?.limit;
       matchingChargingProfile = result?.matchingChargingProfile;
@@ -508,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;
@@ -659,112 +666,266 @@ const convertDeprecatedTemplateKey = (
   }
 };
 
+interface ChargingProfilesLimit {
+  limit: number;
+  matchingChargingProfile: ChargingProfile;
+}
+
 /**
  * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
  *
  * @param chargingProfiles -
  * @param logPrefix -
- * @returns
+ * @returns ChargingProfilesLimit
  */
 const getLimitFromChargingProfiles = (
+  chargingStation: ChargingStation,
+  connectorId: number,
   chargingProfiles: ChargingProfile[],
   logPrefix: string,
-):
-  | {
-      limit: number;
-      matchingChargingProfile: ChargingProfile;
-    }
-  | undefined => {
+): ChargingProfilesLimit | undefined => {
   const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
   const currentDate = new Date();
+  const connectorStatus = chargingStation.getConnectorStatus(connectorId);
   for (const chargingProfile of chargingProfiles) {
-    // Set helpers
+    if (
+      (isValidDate(chargingProfile.validFrom) &&
+        isBefore(currentDate, chargingProfile.validFrom!)) ||
+      (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
+    ) {
+      logger.debug(
+        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${
+          chargingProfile.chargingProfileId
+        } is not valid for the current date ${currentDate.toISOString()}`,
+      );
+      continue;
+    }
     const chargingSchedule = chargingProfile.chargingSchedule;
-    if (!chargingSchedule?.startSchedule) {
-      logger.warn(
-        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`,
+    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`,
       );
+      // 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`,
       );
-      chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!;
+      chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
+    }
+    if (
+      chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
+      isNullOrUndefined(chargingProfile.recurrencyKind)
+    ) {
+      logger.error(
+        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
+      );
+      continue;
     }
-    // Adjust recurring schedule
     if (chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING) {
-      switch (chargingProfile.recurrencyKind) {
-        case RecurrencyKindType.DAILY:
-          if (isYesterday(chargingSchedule.startSchedule)) {
-            addDays(chargingSchedule.startSchedule, 1);
-          } else if (isTomorrow(chargingSchedule.startSchedule)) {
-            subDays(chargingSchedule.startSchedule, 1);
-          }
-          break;
-        case RecurrencyKindType.WEEKLY:
-          if (isBefore(chargingSchedule.startSchedule, startOfWeek(currentDate))) {
-            addWeeks(chargingSchedule.startSchedule, 1);
-          } else if (isAfter(chargingSchedule.startSchedule, endOfWeek(currentDate))) {
-            subWeeks(chargingSchedule.startSchedule, 1);
-          }
-          break;
-      }
+      prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
+    } else if (
+      chargingProfile.chargingProfileKind === ChargingProfileKindType.RELATIVE &&
+      connectorStatus?.transactionStarted
+    ) {
+      chargingSchedule.startSchedule = connectorStatus?.transactionStart;
+    }
+    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 (
-      isAfter(addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration!), currentDate)
+      isValidDate(chargingSchedule?.startSchedule) &&
+      isWithinInterval(currentDate, {
+        start: chargingSchedule.startSchedule!,
+        end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
+      })
     ) {
-      let lastButOneSchedule: ChargingSchedulePeriod | undefined;
-      // Search the right schedule period
-      for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
-        // Handling of only one period
+      if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
+        const chargingSchedulePeriodCompareFn = (
+          a: ChargingSchedulePeriod,
+          b: ChargingSchedulePeriod,
+        ) => a.startPeriod - b.startPeriod;
         if (
-          chargingSchedule.chargingSchedulePeriod.length === 1 &&
-          schedulePeriod.startPeriod === 0
+          isArraySorted<ChargingSchedulePeriod>(
+            chargingSchedule.chargingSchedulePeriod,
+            chargingSchedulePeriodCompareFn,
+          ) === false
         ) {
-          const result = {
-            limit: schedulePeriod.limit,
-            matchingChargingProfile: chargingProfile,
-          };
-          logger.debug(debugLogMsg, result);
-          return result;
+          logger.warn(
+            `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
+          );
+          chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
         }
-        // Find the right schedule period
-        if (
-          isAfter(
-            addSeconds(chargingSchedule.startSchedule, schedulePeriod.startPeriod),
-            currentDate,
-          )
-        ) {
-          // Found the schedule: last but one is the correct one
-          const result = {
-            limit: lastButOneSchedule!.limit,
-            matchingChargingProfile: chargingProfile,
-          };
-          logger.debug(debugLogMsg, result);
-          return result;
+        // 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;
         }
-        // Keep it
-        lastButOneSchedule = schedulePeriod;
-        // Handle the last schedule period
-        if (
-          schedulePeriod.startPeriod ===
-          chargingSchedule.chargingSchedulePeriod[
-            chargingSchedule.chargingSchedulePeriod.length - 1
-          ].startPeriod
-        ) {
-          const result = {
-            limit: lastButOneSchedule.limit,
+        // Handle only one schedule period
+        if (chargingSchedule.chargingSchedulePeriod.length === 1) {
+          const result: ChargingProfilesLimit = {
+            limit: chargingSchedule.chargingSchedulePeriod[0].limit,
             matchingChargingProfile: chargingProfile,
           };
           logger.debug(debugLogMsg, result);
           return result;
         }
+        let lastButOneSchedule: ChargingSchedulePeriod | undefined;
+        // Search for the right schedule period
+        for (const [index, schedulePeriod] of chargingSchedule.chargingSchedulePeriod.entries()) {
+          // Find the right schedule period
+          if (
+            isAfter(
+              addSeconds(chargingSchedule.startSchedule!, schedulePeriod.startPeriod),
+              currentDate,
+            )
+          ) {
+            // Found the schedule period: last but one is the correct one
+            const result: ChargingProfilesLimit = {
+              limit: lastButOneSchedule!.limit,
+              matchingChargingProfile: chargingProfile,
+            };
+            logger.debug(debugLogMsg, result);
+            return result;
+          }
+          // Keep it
+          lastButOneSchedule = schedulePeriod;
+          // Handle the last schedule period within the charging profile duration
+          if (
+            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,
+              matchingChargingProfile: chargingProfile,
+            };
+            logger.debug(debugLogMsg, result);
+            return result;
+          }
+        }
       }
     }
   }
 };
 
+/**
+ *  Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
+ *
+ * @param chargingProfile -
+ * @param currentDate -
+ * @param logPrefix -
+ */
+const prepareRecurringChargingProfile = (
+  chargingProfile: ChargingProfile,
+  currentDate: Date,
+  logPrefix: string,
+) => {
+  const chargingSchedule = chargingProfile.chargingSchedule;
+  let recurringInterval: Interval;
+  switch (chargingProfile.recurrencyKind) {
+    case RecurrencyKindType.DAILY:
+      recurringInterval = {
+        start: chargingSchedule.startSchedule!,
+        end: addDays(chargingSchedule.startSchedule!, 1),
+      };
+      checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
+      if (
+        !isWithinInterval(currentDate, recurringInterval) &&
+        isBefore(recurringInterval.end, currentDate)
+      ) {
+        chargingSchedule.startSchedule = addDays(
+          recurringInterval.start,
+          differenceInDays(currentDate, recurringInterval.start),
+        );
+        recurringInterval = {
+          start: chargingSchedule.startSchedule,
+          end: addDays(chargingSchedule.startSchedule, 1),
+        };
+      }
+      break;
+    case RecurrencyKindType.WEEKLY:
+      recurringInterval = {
+        start: chargingSchedule.startSchedule!,
+        end: addWeeks(chargingSchedule.startSchedule!, 1),
+      };
+      checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
+      if (
+        !isWithinInterval(currentDate, recurringInterval) &&
+        isBefore(recurringInterval.end, currentDate)
+      ) {
+        chargingSchedule.startSchedule = addWeeks(
+          recurringInterval.start,
+          differenceInWeeks(currentDate, recurringInterval.start),
+        );
+        recurringInterval = {
+          start: chargingSchedule.startSchedule,
+          end: addWeeks(chargingSchedule.startSchedule, 1),
+        };
+      }
+      break;
+  }
+  if (!isWithinInterval(currentDate, recurringInterval!)) {
+    logger.error(
+      `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
+        chargingProfile.recurrencyKind
+      } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
+        recurringInterval!.start,
+      ).toISOString()}, ${toDate(
+        recurringInterval!.end,
+      ).toISOString()}] is not properly translated to current date ${currentDate.toISOString()} `,
+    );
+  }
+};
+
+const checkRecurringChargingProfileDuration = (
+  chargingProfile: ChargingProfile,
+  interval: Interval,
+  logPrefix: string,
+) => {
+  if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
+    logger.warn(
+      `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
+        chargingProfile.chargingProfileKind
+      } charging profile id ${
+        chargingProfile.chargingProfileId
+      } 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);
+  }
+};
+
 const getRandomSerialNumberSuffix = (params?: {
   randomBytesLength?: number;
   upperCase?: boolean;