feat: make get composite schedule closer to OCPP 1.6 specs
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
index 6cd270ffee29855891f0d79a6565af6547283c85..05773f0159f3f1378dd78af4cf2a46ac3395b373 100644 (file)
@@ -14,6 +14,7 @@ import {
   isAfter,
   isBefore,
   isDate,
+  isPast,
   isWithinInterval,
   toDate,
 } from 'date-fns';
@@ -42,6 +43,8 @@ import {
   type OCPP20BootNotificationRequest,
   OCPPVersion,
   RecurrencyKindType,
+  type Reservation,
+  ReservationTerminationReason,
   StandardParametersKey,
   SupportedFeatureProfiles,
   Voltage,
@@ -82,17 +85,49 @@ export const getChargingStationId = (
       )}${idSuffix}`;
 };
 
-export const countReservableConnectors = (connectors: Map<number, ConnectorStatus>) => {
-  let reservableConnectors = 0;
+export const hasReservationExpired = (reservation: Reservation): boolean => {
+  return isPast(reservation.expiryDate);
+};
+
+export const removeExpiredReservations = async (
+  chargingStation: ChargingStation,
+): Promise<void> => {
+  if (chargingStation.hasEvses) {
+    for (const evseStatus of chargingStation.evses.values()) {
+      for (const connectorStatus of evseStatus.connectors.values()) {
+        if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
+          await chargingStation.removeReservation(
+            connectorStatus.reservation,
+            ReservationTerminationReason.EXPIRED,
+          );
+        }
+      }
+    }
+  } else {
+    for (const connectorStatus of chargingStation.connectors.values()) {
+      if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
+        await chargingStation.removeReservation(
+          connectorStatus.reservation,
+          ReservationTerminationReason.EXPIRED,
+        );
+      }
+    }
+  }
+};
+
+export const getNumberOfReservableConnectors = (
+  connectors: Map<number, ConnectorStatus>,
+): number => {
+  let numberOfReservableConnectors = 0;
   for (const [connectorId, connectorStatus] of connectors) {
     if (connectorId === 0) {
       continue;
     }
     if (connectorStatus.status === ConnectorStatusEnum.Available) {
-      ++reservableConnectors;
+      ++numberOfReservableConnectors;
     }
   }
-  return reservableConnectors;
+  return numberOfReservableConnectors;
 };
 
 export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
@@ -222,7 +257,7 @@ export const checkConnectorsConfiguration = (
   templateMaxConnectors: number;
   templateMaxAvailableConnectors: number;
 } => {
-  const configuredMaxConnectors = getConfiguredNumberOfConnectors(stationTemplate);
+  const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate);
   checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile);
   const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
   checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
@@ -485,18 +520,11 @@ export const getChargingStationConnectorChargingProfilesPowerLimit = (
 ): number | undefined => {
   let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
   // 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 charging profiles on connector 0 and sort by stack level
-  if (isNotEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)) {
-    chargingProfiles.push(
-      ...cloneObject<ChargingProfile[]>(
-        chargingStation.getConnectorStatus(0)!.chargingProfiles!,
-      ).sort((a, b) => b.stackLevel - a.stackLevel),
-    );
-  }
+  const chargingProfiles = cloneObject<ChargingProfile[]>(
+    (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? []).concat(
+      chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [],
+    ),
+  ).sort((a, b) => b.stackLevel - a.stackLevel);
   if (isNotEmptyArray(chargingProfiles)) {
     const result = getLimitFromChargingProfiles(
       chargingStation,
@@ -587,16 +615,16 @@ export const waitChargingStationEvents = async (
   });
 };
 
-const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
-  let configuredMaxConnectors = 0;
+const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
+  let configuredMaxNumberOfConnectors = 0;
   if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
     const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
-    configuredMaxConnectors =
+    configuredMaxNumberOfConnectors =
       numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
   } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
-    configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
+    configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
   } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
-    configuredMaxConnectors = stationTemplate.Connectors[0]
+    configuredMaxNumberOfConnectors = stationTemplate.Connectors[0]
       ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
       : getMaxNumberOfConnectors(stationTemplate.Connectors);
   } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
@@ -604,10 +632,12 @@ const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplat
       if (evse === '0') {
         continue;
       }
-      configuredMaxConnectors += getMaxNumberOfConnectors(stationTemplate.Evses[evse].Connectors);
+      configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
+        stationTemplate.Evses[evse].Connectors,
+      );
     }
   }
-  return configuredMaxConnectors;
+  return configuredMaxNumberOfConnectors;
 };
 
 const checkConfiguredMaxConnectors = (
@@ -704,6 +734,12 @@ const getLimitFromChargingProfiles = (
   const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
   const currentDate = new Date();
   const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+  if (!isArraySorted(chargingProfiles, (a, b) => b.stackLevel - a.stackLevel)) {
+    logger.warn(
+      `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profiles are not sorted by stack level. Trying to sort them`,
+    );
+    chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel);
+  }
   for (const chargingProfile of chargingProfiles) {
     const chargingSchedule = chargingProfile.chargingSchedule;
     if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
@@ -823,7 +859,7 @@ const getLimitFromChargingProfiles = (
   }
 };
 
-const canProceedChargingProfile = (
+export const canProceedChargingProfile = (
   chargingProfile: ChargingProfile,
   currentDate: Date,
   logPrefix: string,
@@ -842,7 +878,7 @@ const canProceedChargingProfile = (
   const chargingSchedule = chargingProfile.chargingSchedule;
   if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
     logger.error(
-      `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has (still) no startSchedule defined`,
+      `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
     );
     return false;
   }
@@ -855,7 +891,7 @@ const canProceedChargingProfile = (
   return true;
 };
 
-const canProceedRecurringChargingProfile = (
+export const canProceedRecurringChargingProfile = (
   chargingProfile: ChargingProfile,
   logPrefix: string,
 ): boolean => {
@@ -878,7 +914,7 @@ const canProceedRecurringChargingProfile = (
  * @param currentDate -
  * @param logPrefix -
  */
-const prepareRecurringChargingProfile = (
+export const prepareRecurringChargingProfile = (
   chargingProfile: ChargingProfile,
   currentDate: Date,
   logPrefix: string,