build(ci): fix linter errors
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
index 1632f28b13514f18798ebf4992d5af23360922c2..15bd2c1f48cfb86438636f3f245c1d36912cca7c 100644 (file)
@@ -1,10 +1,12 @@
 import { createHash, randomBytes } from 'node:crypto';
 import type { EventEmitter } from 'node:events';
 import { basename, dirname, join } from 'node:path';
+import { env } from 'node:process';
 import { fileURLToPath } from 'node:url';
 
 import chalk from 'chalk';
 import {
+  type Interval,
   addDays,
   addSeconds,
   addWeeks,
@@ -16,6 +18,7 @@ import {
   isDate,
   isPast,
   isWithinInterval,
+  maxTime,
   toDate,
 } from 'date-fns';
 
@@ -31,6 +34,7 @@ import {
   ChargingProfileKindType,
   ChargingRateUnitType,
   type ChargingSchedulePeriod,
+  type ChargingStationConfiguration,
   type ChargingStationInfo,
   type ChargingStationTemplate,
   ChargingStationWorkerMessageEvents,
@@ -72,10 +76,13 @@ const moduleName = 'Helpers';
 
 export const getChargingStationId = (
   index: number,
-  stationTemplate: ChargingStationTemplate,
+  stationTemplate: ChargingStationTemplate | undefined,
 ): string => {
+  if (stationTemplate === undefined) {
+    return "Unknown 'chargingStationId'";
+  }
   // In case of multiple instances: add instance index to charging station id
-  const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
+  const instanceIndex = env.CF_INSTANCE_INDEX ?? 0;
   const idSuffix = stationTemplate?.nameSuffix ?? '';
   const idStr = `000000000${index.toString()}`;
   return stationTemplate?.fixedName
@@ -173,9 +180,9 @@ export const getPhaseRotationValue = (
   } else if (connectorId > 0 && numberOfPhases === 0) {
     return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
     // AC
-  } else if (connectorId > 0 && numberOfPhases === 1) {
+  } else if (connectorId >= 0 && numberOfPhases === 1) {
     return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
-  } else if (connectorId > 0 && numberOfPhases === 3) {
+  } else if (connectorId >= 0 && numberOfPhases === 3) {
     return `${connectorId}.${ConnectorPhaseRotation.RST}`;
   }
 };
@@ -248,6 +255,23 @@ export const checkTemplate = (
   }
 };
 
+export const checkConfiguration = (
+  stationConfiguration: ChargingStationConfiguration | undefined,
+  logPrefix: string,
+  configurationFile: string,
+): void => {
+  if (isNullOrUndefined(stationConfiguration)) {
+    const errorMsg = `Failed to read charging station configuration file ${configurationFile}`;
+    logger.error(`${logPrefix} ${errorMsg}`);
+    throw new BaseError(errorMsg);
+  }
+  if (isEmptyObject(stationConfiguration!)) {
+    const errorMsg = `Empty charging station configuration from file ${configurationFile}`;
+    logger.error(`${logPrefix} ${errorMsg}`);
+    throw new BaseError(errorMsg);
+  }
+};
+
 export const checkConnectorsConfiguration = (
   stationTemplate: ChargingStationTemplate,
   logPrefix: string,
@@ -261,7 +285,7 @@ export const checkConnectorsConfiguration = (
   checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile);
   const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
   checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
-  const templateMaxAvailableConnectors = stationTemplate.Connectors![0]
+  const templateMaxAvailableConnectors = stationTemplate.Connectors?.[0]
     ? templateMaxConnectors - 1
     : templateMaxConnectors;
   if (
@@ -338,6 +362,12 @@ export const initializeConnectorsMapStatus = (
 };
 
 export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
+  connectorStatus.chargingProfiles =
+    connectorStatus.transactionId && isNotEmptyArray(connectorStatus.chargingProfiles)
+      ? connectorStatus.chargingProfiles?.filter(
+          (chargingProfile) => chargingProfile.transactionId !== connectorStatus.transactionId,
+        )
+      : [];
   connectorStatus.idTagLocalAuthorized = false;
   connectorStatus.idTagAuthorized = false;
   connectorStatus.transactionRemoteStarted = false;
@@ -355,7 +385,7 @@ export const createBootNotificationRequest = (
   stationInfo: ChargingStationInfo,
   bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp,
 ): BootNotificationRequest => {
-  const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
+  const ocppVersion = stationInfo.ocppVersion!;
   switch (ocppVersion) {
     case OCPPVersion.VERSION_16:
       return {
@@ -445,12 +475,9 @@ export const stationTemplateToStationInfo = (
 export const createSerialNumber = (
   stationTemplate: ChargingStationTemplate,
   stationInfo: ChargingStationInfo,
-  params: {
+  params?: {
     randomSerialNumberUpperCase?: boolean;
     randomSerialNumber?: boolean;
-  } = {
-    randomSerialNumberUpperCase: true,
-    randomSerialNumber: true,
   },
 ): void => {
   params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
@@ -514,17 +541,36 @@ export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInf
   return unitDivider;
 };
 
+/**
+ * Gets the connector cloned charging profiles applying a power limitation
+ * and sorted by connector id descending then stack level descending
+ *
+ * @param chargingStation -
+ * @param connectorId -
+ * @returns connector charging profiles array
+ */
+export const getConnectorChargingProfiles = (
+  chargingStation: ChargingStation,
+  connectorId: number,
+) => {
+  return cloneObject<ChargingProfile[]>(
+    (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
+      .sort((a, b) => b.stackLevel - a.stackLevel)
+      .concat(
+        (chargingStation.getConnectorStatus(0)?.chargingProfiles ?? []).sort(
+          (a, b) => b.stackLevel - a.stackLevel,
+        ),
+      ),
+  );
+};
+
 export const getChargingStationConnectorChargingProfilesPowerLimit = (
   chargingStation: ChargingStation,
   connectorId: number,
 ): 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 ?? []).concat(
-      chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [],
-    ),
-  ).sort((a, b) => b.stackLevel - a.stackLevel);
+  let limit: number | undefined, chargingProfile: ChargingProfile | undefined;
+  // Get charging profiles sorted by connector id then stack level
+  const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId);
   if (isNotEmptyArray(chargingProfiles)) {
     const result = getLimitFromChargingProfiles(
       chargingStation,
@@ -534,31 +580,29 @@ export const getChargingStationConnectorChargingProfilesPowerLimit = (
     );
     if (!isNullOrUndefined(result)) {
       limit = result?.limit;
-      matchingChargingProfile = result?.matchingChargingProfile;
-      switch (chargingStation.getCurrentOutType()) {
+      chargingProfile = result?.chargingProfile;
+      switch (chargingStation.stationInfo?.currentOutType) {
         case CurrentType.AC:
           limit =
-            matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
-            ChargingRateUnitType.WATT
+            chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
               ? limit
               : ACElectricUtils.powerTotal(
                   chargingStation.getNumberOfPhases(),
-                  chargingStation.getVoltageOut(),
+                  chargingStation.stationInfo.voltageOut!,
                   limit!,
                 );
           break;
         case CurrentType.DC:
           limit =
-            matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
-            ChargingRateUnitType.WATT
+            chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
               ? limit
-              : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
+              : DCElectricUtils.power(chargingStation.stationInfo.voltageOut!, limit!);
       }
       const connectorMaximumPower =
-        chargingStation.getMaximumPower() / chargingStation.powerDivider;
+        chargingStation.stationInfo.maximumPower! / chargingStation.powerDivider;
       if (limit! > connectorMaximumPower) {
         logger.error(
-          `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
+          `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
           result,
         );
         limit = connectorMaximumPower;
@@ -624,7 +668,7 @@ const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemp
   } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
     configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
   } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
-    configuredMaxNumberOfConnectors = stationTemplate.Connectors[0]
+    configuredMaxNumberOfConnectors = stationTemplate.Connectors?.[0]
       ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
       : getMaxNumberOfConnectors(stationTemplate.Connectors);
   } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
@@ -688,12 +732,12 @@ const warnDeprecatedTemplateKey = (
   templateFile: string,
   logMsgToAppend = '',
 ): void => {
-  if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
+  if (!isUndefined(template?.[key as keyof ChargingStationTemplate])) {
     const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
       isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
     }`;
     logger.warn(`${logPrefix} ${logMsg}`);
-    console.warn(chalk.yellow(`${logMsg}`));
+    console.warn(`${chalk.green(logPrefix)} ${chalk.yellow(logMsg)}`);
   }
 };
 
@@ -702,7 +746,7 @@ const convertDeprecatedTemplateKey = (
   deprecatedKey: string,
   key?: string,
 ): void => {
-  if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
+  if (!isUndefined(template?.[deprecatedKey as keyof ChargingStationTemplate])) {
     if (!isUndefined(key)) {
       (template as unknown as Record<string, unknown>)[key!] =
         template[deprecatedKey as keyof ChargingStationTemplate];
@@ -713,11 +757,11 @@ const convertDeprecatedTemplateKey = (
 
 interface ChargingProfilesLimit {
   limit: number;
-  matchingChargingProfile: ChargingProfile;
+  chargingProfile: ChargingProfile;
 }
 
 /**
- * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
+ * Charging profiles shall already be sorted by connector id descending then stack level descending
  *
  * @param chargingStation -
  * @param connectorId -
@@ -734,27 +778,34 @@ 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)) {
+    if (isNullOrUndefined(chargingSchedule?.startSchedule) && connectorStatus?.transactionStarted) {
       logger.debug(
         `${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 (!isDate(chargingSchedule?.startSchedule)) {
+    if (
+      !isNullOrUndefined(chargingSchedule?.startSchedule) &&
+      !isDate(chargingSchedule?.startSchedule)
+    ) {
       logger.warn(
-        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
+        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`,
       );
       chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
     }
+    if (
+      !isNullOrUndefined(chargingSchedule?.startSchedule) &&
+      isNullOrUndefined(chargingSchedule?.duration)
+    ) {
+      logger.debug(
+        `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: 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
+      chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule!);
+    }
     if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
       continue;
     }
@@ -763,7 +814,6 @@ const getLimitFromChargingProfiles = (
     }
     // Check if the charging profile is active
     if (
-      isValidTime(chargingSchedule?.startSchedule) &&
       isWithinInterval(currentDate, {
         start: chargingSchedule.startSchedule!,
         end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
@@ -775,17 +825,17 @@ const getLimitFromChargingProfiles = (
           b: ChargingSchedulePeriod,
         ) => a.startPeriod - b.startPeriod;
         if (
-          isArraySorted<ChargingSchedulePeriod>(
+          !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
+        // Check if the first schedule period startPeriod property 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`,
@@ -796,7 +846,7 @@ const getLimitFromChargingProfiles = (
         if (chargingSchedule.chargingSchedulePeriod.length === 1) {
           const result: ChargingProfilesLimit = {
             limit: chargingSchedule.chargingSchedulePeriod[0].limit,
-            matchingChargingProfile: chargingProfile,
+            chargingProfile,
           };
           logger.debug(debugLogMsg, result);
           return result;
@@ -817,7 +867,7 @@ const getLimitFromChargingProfiles = (
             // Found the schedule period: previous is the correct one
             const result: ChargingProfilesLimit = {
               limit: previousChargingSchedulePeriod!.limit,
-              matchingChargingProfile: chargingProfile,
+              chargingProfile,
             };
             logger.debug(debugLogMsg, result);
             return result;
@@ -828,18 +878,17 @@ const getLimitFromChargingProfiles = (
           if (
             index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
             (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
-              chargingSchedule.duration! >
-                differenceInSeconds(
-                  addSeconds(
-                    chargingSchedule.startSchedule!,
-                    chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
-                  ),
+              differenceInSeconds(
+                addSeconds(
                   chargingSchedule.startSchedule!,
-                ))
+                  chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
+                ),
+                chargingSchedule.startSchedule!,
+              ) > chargingSchedule.duration!)
           ) {
             const result: ChargingProfilesLimit = {
               limit: previousChargingSchedulePeriod.limit,
-              matchingChargingProfile: chargingProfile,
+              chargingProfile,
             };
             logger.debug(debugLogMsg, result);
             return result;
@@ -864,8 +913,16 @@ export const prepareChargingProfileKind = (
       prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
       break;
     case ChargingProfileKindType.RELATIVE:
-      connectorStatus?.transactionStarted &&
-        (chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart);
+      if (!isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)) {
+        logger.warn(
+          `${logPrefix} ${moduleName}.prepareChargingProfileKind: Relative charging profile id ${chargingProfile.chargingProfileId} has a startSchedule property defined. It will be ignored or used if the connector has a transaction started`,
+        );
+        delete chargingProfile.chargingSchedule.startSchedule;
+      }
+      if (connectorStatus?.transactionStarted) {
+        chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart;
+      }
+      // FIXME: Handle relative charging profile duration
       break;
   }
   return true;
@@ -887,16 +944,30 @@ export const canProceedChargingProfile = (
     );
     return false;
   }
-  const chargingSchedule = chargingProfile.chargingSchedule;
-  if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
+  if (
+    isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) ||
+    isNullOrUndefined(chargingProfile.chargingSchedule.duration)
+  ) {
     logger.error(
-      `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
+      `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule or duration defined`,
     );
     return false;
   }
-  if (isNullOrUndefined(chargingSchedule?.duration)) {
+  if (
+    !isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule) &&
+    !isValidTime(chargingProfile.chargingSchedule.startSchedule)
+  ) {
     logger.error(
-      `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
+      `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`,
+    );
+    return false;
+  }
+  if (
+    !isNullOrUndefined(chargingProfile.chargingSchedule.duration) &&
+    !Number.isSafeInteger(chargingProfile.chargingSchedule.duration)
+  ) {
+    logger.error(
+      `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has non integer duration defined`,
     );
     return false;
   }
@@ -916,6 +987,15 @@ const canProceedRecurringChargingProfile = (
     );
     return false;
   }
+  if (
+    chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
+    isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)
+  ) {
+    logger.error(
+      `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
+    );
+    return false;
+  }
   return true;
 };
 
@@ -979,7 +1059,7 @@ const prepareRecurringChargingProfile = (
       break;
     default:
       logger.error(
-        `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
+        `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId} is not supported`,
       );
   }
   if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {