refactor: factor out charging schedule composition code
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16IncomingRequestService.ts
index 01d1211dba709b99bbf31dda56f767d956ac4552..0215414862ae253d0a4bfe7455d04f218063ef27 100644 (file)
@@ -6,15 +6,18 @@ import { URL, fileURLToPath } from 'node:url';
 
 import type { JSONSchemaType } from 'ajv';
 import { Client, type FTPResponse } from 'basic-ftp';
-import { addSeconds, isWithinInterval, max, secondsToMilliseconds } from 'date-fns';
+import { addSeconds, differenceInSeconds, isDate, maxTime, secondsToMilliseconds } from 'date-fns';
 import { create } from 'tar';
 
 import { OCPP16Constants } from './OCPP16Constants';
 import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
 import {
   type ChargingStation,
+  canProceedChargingProfile,
   checkChargingStation,
   getConfigurationKey,
+  getConnectorChargingProfiles,
+  prepareChargingProfileKind,
   removeExpiredReservations,
   setConfigurationKeyValue,
 } from '../../../charging-station';
@@ -668,78 +671,107 @@ 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;
     }
-    if (isEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
+    if (
+      isEmptyArray(
+        connectorStatus?.chargingProfiles &&
+          isEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles),
+      )
+    ) {
       return OCPP16Constants.OCPP_RESPONSE_REJECTED;
     }
-    const startDate = new Date();
-    const endDate = addSeconds(startDate, duration);
+    const currentDate = new Date();
+    const compositeScheduleInterval: Interval = {
+      start: currentDate,
+      end: addSeconds(currentDate, duration),
+    };
+    // Get charging profiles sorted by connector id then stack level
+    const chargingProfiles: OCPP16ChargingProfile[] = getConnectorChargingProfiles(
+      chargingStation,
+      connectorId,
+    );
+    let previousCompositeSchedule: OCPP16ChargingSchedule | undefined;
     let compositeSchedule: OCPP16ChargingSchedule | undefined;
-    for (const chargingProfile of chargingStation.getConnectorStatus(connectorId)!
-      .chargingProfiles!) {
+    for (const chargingProfile of chargingProfiles) {
       if (
-        compositeSchedule?.chargingRateUnit &&
-        compositeSchedule.chargingRateUnit !== chargingProfile.chargingSchedule.chargingRateUnit
+        isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule) &&
+        connectorStatus?.transactionStarted
       ) {
-        logger.error(
-          `${chargingStation.logPrefix()} Building composite schedule with different charging rate units is not yet supported, skipping charging profile id ${
+        logger.debug(
+          `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: 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
+        chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
+      }
+      if (
+        !isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule) &&
+        !isDate(chargingProfile.chargingSchedule?.startSchedule)
+      ) {
+        logger.warn(
+          `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
+            chargingProfile.chargingProfileId
+          } startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
+        );
+        chargingProfile.chargingSchedule.startSchedule = convertToDate(
+          chargingProfile.chargingSchedule?.startSchedule,
+        )!;
+      }
+      if (
+        !isNullOrUndefined(chargingProfile.chargingSchedule?.startSchedule) &&
+        isNullOrUndefined(chargingProfile.chargingSchedule?.duration)
+      ) {
+        logger.debug(
+          `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
+            chargingProfile.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
+        chargingProfile.chargingSchedule.duration = differenceInSeconds(
+          maxTime,
+          chargingProfile.chargingSchedule.startSchedule!,
+        );
+      }
+      if (
+        !prepareChargingProfileKind(
+          connectorStatus,
+          chargingProfile,
+          compositeScheduleInterval.start as Date,
+          chargingStation.logPrefix(),
+        )
+      ) {
         continue;
       }
       if (
-        isWithinInterval(chargingProfile.chargingSchedule.startSchedule!, {
-          start: startDate,
-          end: endDate,
-        }) &&
-        isWithinInterval(
-          addSeconds(
-            chargingProfile.chargingSchedule.startSchedule!,
-            chargingProfile.chargingSchedule.duration!,
-          ),
-          {
-            start: startDate,
-            end: endDate,
-          },
+        !canProceedChargingProfile(
+          chargingProfile,
+          compositeScheduleInterval.start as Date,
+          chargingStation.logPrefix(),
         )
       ) {
-        compositeSchedule = {
-          startSchedule: max([
-            compositeSchedule?.startSchedule ?? startDate,
-            chargingProfile.chargingSchedule.startSchedule!,
-          ]),
-          duration: Math.max(
-            compositeSchedule?.duration ?? -Infinity,
-            chargingProfile.chargingSchedule.duration!,
-          ),
-          chargingRateUnit: chargingProfile.chargingSchedule.chargingRateUnit,
-          ...(compositeSchedule?.chargingSchedulePeriod === undefined
-            ? { chargingSchedulePeriod: [] }
-            : {
-                chargingSchedulePeriod: compositeSchedule.chargingSchedulePeriod.concat(
-                  ...chargingProfile.chargingSchedule.chargingSchedulePeriod,
-                ),
-              }),
-          ...(chargingProfile.chargingSchedule.minChargeRate && {
-            minChargeRate: Math.min(
-              compositeSchedule?.minChargeRate ?? Infinity,
-              chargingProfile.chargingSchedule.minChargeRate,
-            ),
-          }),
-        };
+        continue;
       }
+      compositeSchedule = OCPP16ServiceUtils.composeChargingSchedules(
+        previousCompositeSchedule,
+        chargingProfile.chargingSchedule,
+        compositeScheduleInterval,
+      );
+      previousCompositeSchedule = compositeSchedule;
     }
-    return {
-      status: GenericStatus.Accepted,
-      scheduleStart: compositeSchedule?.startSchedule,
-      connectorId,
-      chargingSchedule: compositeSchedule,
-    };
+    if (compositeSchedule) {
+      return {
+        status: GenericStatus.Accepted,
+        scheduleStart: compositeSchedule.startSchedule!,
+        connectorId,
+        chargingSchedule: compositeSchedule,
+      };
+    }
+    return OCPP16Constants.OCPP_RESPONSE_REJECTED;
   }
 
   private handleRequestClearChargingProfile(