feat: add initial support get composite schedule OCPP 1.6 command
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
index daf48ac3475de0b918de6e653fce5c14ce6aadad..1059a1d0c8b4da1215eb1184b1a25614ed32b07e 100644 (file)
@@ -1,28 +1,38 @@
-import crypto from 'crypto';
-import path from 'path';
-import { fileURLToPath } from 'url';
+import crypto from 'node:crypto';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
 
+import chalk from 'chalk';
 import moment from 'moment';
 
-import BaseError from '../exception/BaseError';
-import type { ChargingStationInfo } from '../types/ChargingStationInfo';
+import type { ChargingStation } from './internal';
+import { BaseError } from '../exception';
 import {
   AmpereUnits,
+  type BootNotificationRequest,
+  BootReasonEnumType,
+  type ChargingProfile,
+  ChargingProfileKindType,
+  ChargingRateUnitType,
+  type ChargingSchedulePeriod,
+  type ChargingStationInfo,
   type ChargingStationTemplate,
   CurrentType,
+  type OCPP16BootNotificationRequest,
+  type OCPP20BootNotificationRequest,
+  OCPPVersion,
+  RecurrencyKindType,
   Voltage,
-} from '../types/ChargingStationTemplate';
-import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
-import type { OCPP16BootNotificationRequest } from '../types/ocpp/1.6/Requests';
-import { BootReasonEnumType, OCPP20BootNotificationRequest } from '../types/ocpp/2.0/Requests';
-import type { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
-import { OCPPVersion } from '../types/ocpp/OCPPVersion';
-import type { BootNotificationRequest } from '../types/ocpp/Requests';
-import { WorkerProcessType } from '../types/Worker';
-import Configuration from '../utils/Configuration';
-import Constants from '../utils/Constants';
-import logger from '../utils/Logger';
-import Utils from '../utils/Utils';
+} from '../types';
+import {
+  ACElectricUtils,
+  Configuration,
+  Constants,
+  DCElectricUtils,
+  Utils,
+  logger,
+} from '../utils';
+import { WorkerProcessType } from '../worker';
 
 const moduleName = 'ChargingStationUtils';
 
@@ -37,19 +47,17 @@ export class ChargingStationUtils {
   ): string {
     // In case of multiple instances: add instance index to charging station id
     const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
-    const idSuffix = stationTemplate.nameSuffix ?? '';
-    const idStr = '000000000' + index.toString();
+    const idSuffix = stationTemplate?.nameSuffix ?? '';
+    const idStr = `000000000${index.toString()}`;
     return stationTemplate?.fixedName
       ? stationTemplate.baseName
-      : stationTemplate.baseName +
-          '-' +
-          instanceIndex.toString() +
-          idStr.substring(idStr.length - 4) +
-          idSuffix;
+      : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
+          idStr.length - 4
+        )}${idSuffix}`;
   }
 
   public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
-    const hashBootNotificationRequest = {
+    const chargingStationInfo = {
       chargePointModel: stationTemplate.chargePointModel,
       chargePointVendor: stationTemplate.chargePointVendor,
       ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
@@ -58,12 +66,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,
       }),
@@ -74,12 +76,22 @@ export class ChargingStationUtils {
     return crypto
       .createHash(Constants.DEFAULT_HASH_ALGORITHM)
       .update(
-        JSON.stringify(hashBootNotificationRequest) +
-          ChargingStationUtils.getChargingStationId(index, stationTemplate)
+        `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
+          index,
+          stationTemplate
+        )}`
       )
       .digest('hex');
   }
 
+  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 getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
     const templateConnectors = stationTemplate?.Connectors;
     if (!templateConnectors) {
@@ -106,7 +118,7 @@ export class ChargingStationUtils {
 
   public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
     let configuredMaxConnectors: number;
-    if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
+    if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
       const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
       configuredMaxConnectors =
         numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
@@ -133,7 +145,8 @@ export class ChargingStationUtils {
   }
 
   public static createBootNotificationRequest(
-    stationInfo: ChargingStationInfo
+    stationInfo: ChargingStationInfo,
+    bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
   ): BootNotificationRequest {
     const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
     switch (ocppVersion) {
@@ -162,7 +175,7 @@ export class ChargingStationUtils {
       case OCPPVersion.VERSION_20:
       case OCPPVersion.VERSION_201:
         return {
-          reason: BootReasonEnumType.PowerUp,
+          reason: bootReason,
           chargingStation: {
             model: stationInfo.chargePointModel,
             vendorName: stationInfo.chargePointVendor,
@@ -184,39 +197,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}'${
-          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];
     }
   }
 
@@ -244,7 +255,7 @@ export class ChargingStationUtils {
 
   public static createSerialNumber(
     stationTemplate: ChargingStationTemplate,
-    stationInfo: ChargingStationInfo = {} as ChargingStationInfo,
+    stationInfo: ChargingStationInfo,
     params: {
       randomSerialNumberUpperCase?: boolean;
       randomSerialNumber?: boolean;
@@ -261,21 +272,25 @@ export class ChargingStationUtils {
           upperCase: params.randomSerialNumberUpperCase,
         })
       : '';
-    stationInfo.chargePointSerialNumber =
-      stationTemplate?.chargePointSerialNumberPrefix &&
-      stationTemplate.chargePointSerialNumberPrefix + serialNumberSuffix;
-    stationInfo.chargeBoxSerialNumber =
-      stationTemplate?.chargeBoxSerialNumberPrefix &&
-      stationTemplate.chargeBoxSerialNumberPrefix + serialNumberSuffix;
-    stationInfo.meterSerialNumber =
-      stationTemplate?.meterSerialNumberPrefix &&
-      stationTemplate.meterSerialNumberPrefix + serialNumberSuffix;
+    stationInfo.chargePointSerialNumber = Utils.isNotEmptyString(
+      stationTemplate?.chargePointSerialNumberPrefix
+    )
+      ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
+      : undefined;
+    stationInfo.chargeBoxSerialNumber = Utils.isNotEmptyString(
+      stationTemplate?.chargeBoxSerialNumberPrefix
+    )
+      ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
+      : undefined;
+    stationInfo.meterSerialNumber = Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix)
+      ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
+      : undefined;
   }
 
   public static propagateSerialNumber(
     stationTemplate: ChargingStationTemplate,
     stationInfoSrc: ChargingStationInfo,
-    stationInfoDst: ChargingStationInfo = {} as ChargingStationInfo
+    stationInfoDst: ChargingStationInfo
   ) {
     if (!stationInfoSrc || !stationTemplate) {
       throw new BaseError(
@@ -309,6 +324,127 @@ export class ChargingStationUtils {
     return unitDivider;
   }
 
+  public static getChargingStationConnectorChargingProfilesPowerLimit(
+    chargingStation: ChargingStation,
+    connectorId: number
+  ): number | undefined {
+    let limit: number, matchingChargingProfile: ChargingProfile;
+    // Get charging profiles for connector and sort by stack level
+    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)
+      );
+    }
+    if (Utils.isNotEmptyArray(chargingProfiles)) {
+      const result = ChargingStationUtils.getLimitFromChargingProfiles(
+        chargingProfiles,
+        chargingStation.logPrefix()
+      );
+      if (!Utils.isNullOrUndefined(result)) {
+        limit = result?.limit;
+        matchingChargingProfile = result?.matchingChargingProfile;
+        switch (chargingStation.getCurrentOutType()) {
+          case CurrentType.AC:
+            limit =
+              matchingChargingProfile.chargingSchedule.chargingRateUnit ===
+              ChargingRateUnitType.WATT
+                ? limit
+                : ACElectricUtils.powerTotal(
+                    chargingStation.getNumberOfPhases(),
+                    chargingStation.getVoltageOut(),
+                    limit
+                  );
+            break;
+          case CurrentType.DC:
+            limit =
+              matchingChargingProfile.chargingSchedule.chargingRateUnit ===
+              ChargingRateUnitType.WATT
+                ? limit
+                : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
+        }
+        const connectorMaximumPower =
+          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`,
+            result
+          );
+          limit = connectorMaximumPower;
+        }
+      }
+    }
+    return limit;
+  }
+
+  public static getDefaultVoltageOut(
+    currentType: CurrentType,
+    templateFile: string,
+    logPrefix: string
+  ): Voltage {
+    const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
+    let defaultVoltageOut: number;
+    switch (currentType) {
+      case CurrentType.AC:
+        defaultVoltageOut = Voltage.VOLTAGE_230;
+        break;
+      case CurrentType.DC:
+        defaultVoltageOut = Voltage.VOLTAGE_400;
+        break;
+      default:
+        logger.error(`${logPrefix} ${errMsg}`);
+        throw new BaseError(errMsg);
+    }
+    return defaultVoltageOut;
+  }
+
+  public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
+    return (
+      stationInfo.idTagsFile &&
+      path.join(
+        path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
+        'assets',
+        path.basename(stationInfo.idTagsFile)
+      )
+    );
+  }
+
+  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)
    *
@@ -316,7 +452,7 @@ export class ChargingStationUtils {
    * @param logPrefix -
    * @returns
    */
-  public static getLimitFromChargingProfiles(
+  private static getLimitFromChargingProfiles(
     chargingProfiles: ChargingProfile[],
     logPrefix: string
   ): {
@@ -324,10 +460,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 (
@@ -335,8 +477,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(),
@@ -406,38 +552,6 @@ export class ChargingStationUtils {
     return null;
   }
 
-  public static getDefaultVoltageOut(
-    currentType: CurrentType,
-    templateFile: string,
-    logPrefix: string
-  ): Voltage {
-    const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
-    let defaultVoltageOut: number;
-    switch (currentType) {
-      case CurrentType.AC:
-        defaultVoltageOut = Voltage.VOLTAGE_230;
-        break;
-      case CurrentType.DC:
-        defaultVoltageOut = Voltage.VOLTAGE_400;
-        break;
-      default:
-        logger.error(`${logPrefix} ${errMsg}`);
-        throw new BaseError(errMsg);
-    }
-    return defaultVoltageOut;
-  }
-
-  public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined {
-    return (
-      stationInfo.authorizationFile &&
-      path.join(
-        path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
-        'assets',
-        path.basename(stationInfo.authorizationFile)
-      )
-    );
-  }
-
   private static getRandomSerialNumberSuffix(params?: {
     randomBytesLength?: number;
     upperCase?: boolean;