feat: save connectors/evses map in charging station configuration file
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
index 22e712d03432a0e4c1a4557f0dbde07db42f54cd..9f93e5ffe0c8be18c33d26604f3d96af65be7682 100644 (file)
@@ -2,12 +2,14 @@ import crypto from 'node:crypto';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
 
+import chalk from 'chalk';
 import moment from 'moment';
 
 import type { ChargingStation } from './internal';
 import { BaseError } from '../exception';
 import {
   AmpereUnits,
+  AvailabilityType,
   type BootNotificationRequest,
   BootReasonEnumType,
   type ChargingProfile,
@@ -16,7 +18,9 @@ import {
   type ChargingSchedulePeriod,
   type ChargingStationInfo,
   type ChargingStationTemplate,
+  type ConnectorStatus,
   CurrentType,
+  type EvseTemplate,
   type OCPP16BootNotificationRequest,
   type OCPP20BootNotificationRequest,
   OCPPVersion,
@@ -65,12 +69,6 @@ export class ChargingStationUtils {
       ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
         chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
       }),
-      // FIXME?: Should a firmware version change always reference a new configuration file?
-      ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && {
-        firmwareVersion: stationTemplate.firmwareVersion,
-      }),
-      ...(!Utils.isUndefined(stationTemplate.iccid) && { iccid: stationTemplate.iccid }),
-      ...(!Utils.isUndefined(stationTemplate.imsi) && { imsi: stationTemplate.imsi }),
       ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
         meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
       }),
@@ -89,12 +87,26 @@ export class ChargingStationUtils {
       .digest('hex');
   }
 
-  public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
-    const templateConnectors = stationTemplate?.Connectors;
-    if (!templateConnectors) {
+  public static checkChargingStation(chargingStation: ChargingStation, logPrefix: string): boolean {
+    if (chargingStation.started === false && chargingStation.starting === false) {
+      logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
+      return false;
+    }
+    return true;
+  }
+
+  public static getMaxNumberOfEvses(evses: Record<string, EvseTemplate>): number {
+    if (!evses) {
+      return -1;
+    }
+    return Object.keys(evses).length;
+  }
+
+  public static getMaxNumberOfConnectors(connectors: Record<string, ConnectorStatus>): number {
+    if (!connectors) {
       return -1;
     }
-    return Object.keys(templateConnectors).length;
+    return Object.keys(connectors).length;
   }
 
   public static checkTemplateMaxConnectors(
@@ -113,18 +125,28 @@ export class ChargingStationUtils {
     }
   }
 
-  public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
+  public static getConfiguredNumberOfConnectors(stationInfo: ChargingStationInfo): number {
     let configuredMaxConnectors: number;
-    if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
-      const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
+    if (Utils.isNotEmptyArray(stationInfo.numberOfConnectors) === true) {
+      const numberOfConnectors = stationInfo.numberOfConnectors as number[];
       configuredMaxConnectors =
         numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
-    } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
-      configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
-    } else {
-      configuredMaxConnectors = stationTemplate?.Connectors[0]
-        ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
-        : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
+    } else if (Utils.isUndefined(stationInfo.numberOfConnectors) === false) {
+      configuredMaxConnectors = stationInfo.numberOfConnectors as number;
+    } else if (stationInfo.Connectors && !stationInfo.Evses) {
+      configuredMaxConnectors = stationInfo?.Connectors[0]
+        ? ChargingStationUtils.getMaxNumberOfConnectors(stationInfo.Connectors) - 1
+        : ChargingStationUtils.getMaxNumberOfConnectors(stationInfo.Connectors);
+    } else if (stationInfo.Evses && !stationInfo.Connectors) {
+      configuredMaxConnectors = 0;
+      for (const evse in stationInfo.Evses) {
+        if (evse === '0') {
+          continue;
+        }
+        configuredMaxConnectors += ChargingStationUtils.getMaxNumberOfConnectors(
+          stationInfo.Evses[evse].Connectors
+        );
+      }
     }
     return configuredMaxConnectors;
   }
@@ -141,6 +163,88 @@ export class ChargingStationUtils {
     }
   }
 
+  public static checkStationInfoConnectorStatus(
+    connectorId: number,
+    connectorStatus: ConnectorStatus,
+    logPrefix: string,
+    templateFile: string
+  ): void {
+    if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
+      logger.warn(
+        `${logPrefix} Charging station information from template ${templateFile} with connector ${connectorId} status configuration defined, undefine it`
+      );
+      delete connectorStatus.status;
+    }
+  }
+
+  public static buildConnectorsMap(
+    connectors: Record<string, ConnectorStatus>,
+    logPrefix: string,
+    templateFile: string
+  ): Map<number, ConnectorStatus> {
+    const connectorsMap = new Map<number, ConnectorStatus>();
+    if (ChargingStationUtils.getMaxNumberOfConnectors(connectors) > 0) {
+      for (const connector in connectors) {
+        const connectorStatus = connectors[connector];
+        const connectorId = Utils.convertToInt(connector);
+        ChargingStationUtils.checkStationInfoConnectorStatus(
+          connectorId,
+          connectorStatus,
+          logPrefix,
+          templateFile
+        );
+        connectorsMap.set(connectorId, Utils.cloneObject<ConnectorStatus>(connectorStatus));
+      }
+    } else {
+      logger.warn(
+        `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
+      );
+    }
+    return connectorsMap;
+  }
+
+  public static initializeConnectorsMapStatus(
+    connectors: Map<number, ConnectorStatus>,
+    logPrefix: string
+  ): void {
+    for (const connectorId of connectors.keys()) {
+      if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
+        logger.warn(
+          `${logPrefix} Connector ${connectorId} at initialization has a transaction started: ${
+            connectors.get(connectorId)?.transactionId
+          }`
+        );
+      }
+      if (
+        connectorId === 0 &&
+        Utils.isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
+      ) {
+        connectors.get(connectorId).availability = AvailabilityType.Operative;
+        if (Utils.isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
+          connectors.get(connectorId).chargingProfiles = [];
+        }
+      } else if (
+        connectorId > 0 &&
+        Utils.isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
+      ) {
+        ChargingStationUtils.initializeConnectorStatus(connectors.get(connectorId));
+      }
+    }
+  }
+
+  public static resetConnectorStatus(connectorStatus: ConnectorStatus): void {
+    connectorStatus.idTagLocalAuthorized = false;
+    connectorStatus.idTagAuthorized = false;
+    connectorStatus.transactionRemoteStarted = false;
+    connectorStatus.transactionStarted = false;
+    delete connectorStatus?.localAuthorizeIdTag;
+    delete connectorStatus?.authorizeIdTag;
+    delete connectorStatus?.transactionId;
+    delete connectorStatus?.transactionIdTag;
+    connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
+    delete connectorStatus?.transactionBeginMeterValue;
+  }
+
   public static createBootNotificationRequest(
     stationInfo: ChargingStationInfo,
     bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
@@ -194,39 +298,37 @@ export class ChargingStationUtils {
   }
 
   public static workerPoolInUse(): boolean {
-    return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
+    return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
       Configuration.getWorker().processType
     );
   }
 
   public static workerDynamicPoolInUse(): boolean {
-    return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
+    return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
   }
 
-  public static warnDeprecatedTemplateKey(
-    template: ChargingStationTemplate,
-    key: string,
+  public static warnTemplateKeysDeprecation(
     templateFile: string,
-    logPrefix: string,
-    logMsgToAppend = ''
-  ): void {
-    if (!Utils.isUndefined(template[key])) {
-      logger.warn(
-        `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
-          Utils.isNotEmptyString(logMsgToAppend) && `. ${logMsgToAppend}`
-        }`
+    stationTemplate: ChargingStationTemplate,
+    logPrefix: string
+  ) {
+    const templateKeys: { key: string; deprecatedKey: string }[] = [
+      { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' },
+      { key: 'idTagsFile', deprecatedKey: 'authorizationFile' },
+    ];
+    for (const templateKey of templateKeys) {
+      ChargingStationUtils.warnDeprecatedTemplateKey(
+        stationTemplate,
+        templateKey.deprecatedKey,
+        templateFile,
+        logPrefix,
+        `Use '${templateKey.key}' instead`
+      );
+      ChargingStationUtils.convertDeprecatedTemplateKey(
+        stationTemplate,
+        templateKey.deprecatedKey,
+        templateKey.key
       );
-    }
-  }
-
-  public static convertDeprecatedTemplateKey(
-    template: ChargingStationTemplate,
-    deprecatedKey: string,
-    key: string
-  ): void {
-    if (!Utils.isUndefined(template[deprecatedKey])) {
-      template[key] = template[deprecatedKey] as unknown;
-      delete template[deprecatedKey];
     }
   }
 
@@ -263,7 +365,7 @@ export class ChargingStationUtils {
       randomSerialNumber: true,
     }
   ): void {
-    params = params ?? Constants.EMPTY_OBJECT;
+    params = params ?? {};
     params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
     params.randomSerialNumber = params?.randomSerialNumber ?? true;
     const serialNumberSuffix = params?.randomSerialNumber
@@ -328,17 +430,17 @@ export class ChargingStationUtils {
     connectorId: number
   ): number | undefined {
     let limit: number, matchingChargingProfile: ChargingProfile;
-    let chargingProfiles: ChargingProfile[] = [];
     // Get charging profiles for connector and sort by stack level
-    chargingProfiles = chargingStation
-      .getConnectorStatus(connectorId)
-      ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel);
+    const chargingProfiles =
+      Utils.cloneObject(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)?.sort(
+        (a, b) => b.stackLevel - a.stackLevel
+      ) ?? [];
     // Get profiles on connector 0
     if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
       chargingProfiles.push(
-        ...chargingStation
-          .getConnectorStatus(0)
-          .chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
+        ...Utils.cloneObject(chargingStation.getConnectorStatus(0).chargingProfiles).sort(
+          (a, b) => b.stackLevel - a.stackLevel
+        )
       );
     }
     if (Utils.isNotEmptyArray(chargingProfiles)) {
@@ -405,17 +507,57 @@ export class ChargingStationUtils {
     return defaultVoltageOut;
   }
 
-  public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
+  public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
     return (
-      stationInfo.authorizationFile &&
+      stationInfo.idTagsFile &&
       path.join(
         path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
         'assets',
-        path.basename(stationInfo.authorizationFile)
+        path.basename(stationInfo.idTagsFile)
       )
     );
   }
 
+  private static initializeConnectorStatus(connectorStatus: ConnectorStatus): void {
+    connectorStatus.availability = AvailabilityType.Operative;
+    connectorStatus.idTagLocalAuthorized = false;
+    connectorStatus.idTagAuthorized = false;
+    connectorStatus.transactionRemoteStarted = false;
+    connectorStatus.transactionStarted = false;
+    connectorStatus.energyActiveImportRegisterValue = 0;
+    connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
+    if (Utils.isUndefined(connectorStatus.chargingProfiles)) {
+      connectorStatus.chargingProfiles = [];
+    }
+  }
+
+  private static warnDeprecatedTemplateKey(
+    template: ChargingStationTemplate,
+    key: string,
+    templateFile: string,
+    logPrefix: string,
+    logMsgToAppend = ''
+  ): void {
+    if (!Utils.isUndefined(template[key])) {
+      const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
+        Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
+      }`;
+      logger.warn(`${logPrefix} ${logMsg}`);
+      console.warn(chalk.yellow(`${logMsg}`));
+    }
+  }
+
+  private static convertDeprecatedTemplateKey(
+    template: ChargingStationTemplate,
+    deprecatedKey: string,
+    key: string
+  ): void {
+    if (!Utils.isUndefined(template[deprecatedKey])) {
+      template[key] = template[deprecatedKey] as unknown;
+      delete template[deprecatedKey];
+    }
+  }
+
   /**
    * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
    *
@@ -431,10 +573,16 @@ export class ChargingStationUtils {
     matchingChargingProfile: ChargingProfile;
   } | null {
     const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
+    const currentMoment = moment();
+    const currentDate = new Date();
     for (const chargingProfile of chargingProfiles) {
       // Set helpers
-      const currentMoment = moment();
       const chargingSchedule = chargingProfile.chargingSchedule;
+      if (!chargingSchedule?.startSchedule) {
+        logger.warn(
+          `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
+        );
+      }
       // Check type (recurring) and if it is already active
       // Adjust the daily recurring schedule to today
       if (
@@ -442,8 +590,12 @@ export class ChargingStationUtils {
         chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
         currentMoment.isAfter(chargingSchedule.startSchedule)
       ) {
-        const currentDate = new Date();
-        chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
+        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 = new Date(chargingSchedule.startSchedule);
+        }
         chargingSchedule.startSchedule.setFullYear(
           currentDate.getFullYear(),
           currentDate.getMonth(),