Move hashId to stationInfo
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
index afe9a52fc3ef72f26dc387b4fcd95b9aebf60281..60e143faaa51c55a470b453fe623d3379dc12370 100644 (file)
@@ -1,28 +1,42 @@
-import { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
-import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
+import crypto from 'crypto';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+import moment from 'moment';
+
+import BaseError from '../exception/BaseError';
+import type ChargingStationInfo from '../types/ChargingStationInfo';
 import ChargingStationTemplate, {
   AmpereUnits,
   CurrentType,
   Voltage,
 } from '../types/ChargingStationTemplate';
-import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues';
-
-import { BootNotificationRequest } from '../types/ocpp/Requests';
-import ChargingStation from './ChargingStation';
-import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils';
-import ChargingStationInfo from '../types/ChargingStationInfo';
-import Configuration from '../utils/Configuration';
-import Constants from '../utils/Constants';
-import { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates';
+import type { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates';
+import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
+import type { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
 import { StandardParametersKey } from '../types/ocpp/Configuration';
-import Utils from '../utils/Utils';
+import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues';
+import {
+  BootNotificationRequest,
+  IncomingRequestCommand,
+  RequestCommand,
+} from '../types/ocpp/Requests';
 import { WebSocketCloseEventStatusString } from '../types/WebSocket';
 import { WorkerProcessType } from '../types/Worker';
-import crypto from 'crypto';
+import Configuration from '../utils/Configuration';
+import Constants from '../utils/Constants';
 import logger from '../utils/Logger';
-import moment from 'moment';
+import Utils from '../utils/Utils';
+import type ChargingStation from './ChargingStation';
+import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils';
+
+const moduleName = 'ChargingStationUtils';
 
 export class ChargingStationUtils {
+  private constructor() {
+    // This is intentional
+  }
+
   public static getChargingStationId(
     index: number,
     stationTemplate: ChargingStationTemplate
@@ -40,34 +54,92 @@ export class ChargingStationUtils {
           idSuffix;
   }
 
-  public static getHashId(stationInfo: ChargingStationInfo): string {
+  public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
     const hashBootNotificationRequest = {
-      chargePointModel: stationInfo.chargePointModel,
-      chargePointVendor: stationInfo.chargePointVendor,
-      ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumberPrefix) && {
-        chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumberPrefix,
+      chargePointModel: stationTemplate.chargePointModel,
+      chargePointVendor: stationTemplate.chargePointVendor,
+      ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
+        chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
       }),
-      ...(!Utils.isUndefined(stationInfo.chargePointSerialNumberPrefix) && {
-        chargePointSerialNumber: stationInfo.chargePointSerialNumberPrefix,
+      ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
+        chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
       }),
-      ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
-        firmwareVersion: stationInfo.firmwareVersion,
+      ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && {
+        firmwareVersion: stationTemplate.firmwareVersion,
       }),
-      ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
-      ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
-      ...(!Utils.isUndefined(stationInfo.meterSerialNumberPrefix) && {
-        meterSerialNumber: stationInfo.meterSerialNumberPrefix,
+      ...(!Utils.isUndefined(stationTemplate.iccid) && { iccid: stationTemplate.iccid }),
+      ...(!Utils.isUndefined(stationTemplate.imsi) && { imsi: stationTemplate.imsi }),
+      ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
+        meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
       }),
-      ...(!Utils.isUndefined(stationInfo.meterType) && {
-        meterType: stationInfo.meterType,
+      ...(!Utils.isUndefined(stationTemplate.meterType) && {
+        meterType: stationTemplate.meterType,
       }),
     };
     return crypto
       .createHash(Constants.DEFAULT_HASH_ALGORITHM)
-      .update(JSON.stringify(hashBootNotificationRequest) + stationInfo.chargingStationId)
+      .update(
+        JSON.stringify(hashBootNotificationRequest) +
+          ChargingStationUtils.getChargingStationId(index, stationTemplate)
+      )
       .digest('hex');
   }
 
+  public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
+    const templateConnectors = stationTemplate?.Connectors;
+    if (!templateConnectors) {
+      return -1;
+    }
+    return Object.keys(templateConnectors).length;
+  }
+
+  public static checkTemplateMaxConnectors(
+    templateMaxConnectors: number,
+    templateFile: string,
+    logPrefix: string
+  ): void {
+    if (templateMaxConnectors === 0) {
+      logger.warn(
+        `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
+      );
+    } else if (templateMaxConnectors < 0) {
+      logger.error(
+        `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
+      );
+    }
+  }
+
+  public static getConfiguredNumberOfConnectors(
+    index: number,
+    stationTemplate: ChargingStationTemplate
+  ): number {
+    let configuredMaxConnectors: number;
+    if (!Utils.isEmptyArray(stationTemplate.numberOfConnectors)) {
+      const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
+      // Distribute evenly the number of connectors
+      configuredMaxConnectors = numberOfConnectors[(index - 1) % numberOfConnectors.length];
+    } else if (!Utils.isUndefined(stationTemplate.numberOfConnectors)) {
+      configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
+    } else {
+      configuredMaxConnectors = stationTemplate?.Connectors[0]
+        ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
+        : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
+    }
+    return configuredMaxConnectors;
+  }
+
+  public static checkConfiguredMaxConnectors(
+    configuredMaxConnectors: number,
+    templateFile: string,
+    logPrefix: string
+  ): void {
+    if (configuredMaxConnectors <= 0) {
+      logger.warn(
+        `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
+      );
+    }
+  }
+
   public static createBootNotificationRequest(
     stationInfo: ChargingStationInfo
   ): BootNotificationRequest {
@@ -96,12 +168,12 @@ export class ChargingStationUtils {
 
   public static workerPoolInUse(): boolean {
     return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
-      Configuration.getWorkerProcess()
+      Configuration.getWorker().processType
     );
   }
 
   public static workerDynamicPoolInUse(): boolean {
-    return Configuration.getWorkerProcess() === WorkerProcessType.DYNAMIC_POOL;
+    return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
   }
 
   /**
@@ -157,28 +229,35 @@ export class ChargingStationUtils {
     }
   }
 
-  public static createStationInfoHash(stationInfo: ChargingStationInfo): ChargingStationInfo {
-    const previousInfoHash = stationInfo?.infoHash ?? '';
+  public static stationTemplateToStationInfo(
+    stationTemplate: ChargingStationTemplate
+  ): ChargingStationInfo {
+    stationTemplate = Utils.cloneObject(stationTemplate);
+    delete stationTemplate.power;
+    delete stationTemplate.powerUnit;
+    delete stationTemplate.Configuration;
+    delete stationTemplate.AutomaticTransactionGenerator;
+    delete stationTemplate.chargeBoxSerialNumberPrefix;
+    delete stationTemplate.chargePointSerialNumberPrefix;
+    delete stationTemplate.meterSerialNumberPrefix;
+    return stationTemplate as unknown as ChargingStationInfo;
+  }
+
+  public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
     delete stationInfo.infoHash;
-    const currentInfoHash = crypto
+    stationInfo.infoHash = crypto
       .createHash(Constants.DEFAULT_HASH_ALGORITHM)
       .update(JSON.stringify(stationInfo))
       .digest('hex');
-    if (
-      Utils.isEmptyString(previousInfoHash) ||
-      (!Utils.isEmptyString(previousInfoHash) && currentInfoHash !== previousInfoHash)
-    ) {
-      stationInfo.infoHash = currentInfoHash;
-    } else {
-      stationInfo.infoHash = previousInfoHash;
-    }
-    return stationInfo;
   }
 
   public static createSerialNumber(
-    stationInfo: ChargingStationInfo,
-    existingStationInfo?: ChargingStationInfo | null,
-    params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean } = {
+    stationTemplate: ChargingStationTemplate,
+    stationInfo: ChargingStationInfo = {} as ChargingStationInfo,
+    params: {
+      randomSerialNumberUpperCase?: boolean;
+      randomSerialNumber?: boolean;
+    } = {
       randomSerialNumberUpperCase: true,
       randomSerialNumber: true,
     }
@@ -186,29 +265,41 @@ export class ChargingStationUtils {
     params = params ?? {};
     params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
     params.randomSerialNumber = params?.randomSerialNumber ?? true;
-    if (existingStationInfo) {
-      existingStationInfo?.chargePointSerialNumber &&
-        (stationInfo.chargePointSerialNumber = existingStationInfo.chargePointSerialNumber);
-      existingStationInfo?.chargeBoxSerialNumber &&
-        (stationInfo.chargeBoxSerialNumber = existingStationInfo.chargeBoxSerialNumber);
-      existingStationInfo?.meterSerialNumber &&
-        (stationInfo.meterSerialNumber = existingStationInfo.meterSerialNumber);
-    } else {
-      const serialNumberSuffix = params?.randomSerialNumber
-        ? ChargingStationUtils.getRandomSerialNumberSuffix({
-            upperCase: params.randomSerialNumberUpperCase,
-          })
-        : '';
-      stationInfo.chargePointSerialNumber =
-        stationInfo?.chargePointSerialNumberPrefix &&
-        stationInfo.chargePointSerialNumberPrefix + serialNumberSuffix;
-      stationInfo.chargeBoxSerialNumber =
-        stationInfo?.chargeBoxSerialNumberPrefix &&
-        stationInfo.chargeBoxSerialNumberPrefix + serialNumberSuffix;
-      stationInfo.meterSerialNumber =
-        stationInfo?.meterSerialNumberPrefix &&
-        stationInfo.meterSerialNumberPrefix + serialNumberSuffix;
+    const serialNumberSuffix = params?.randomSerialNumber
+      ? ChargingStationUtils.getRandomSerialNumberSuffix({
+          upperCase: params.randomSerialNumberUpperCase,
+        })
+      : '';
+    stationInfo.chargePointSerialNumber =
+      stationTemplate?.chargePointSerialNumberPrefix &&
+      stationTemplate.chargePointSerialNumberPrefix + serialNumberSuffix;
+    stationInfo.chargeBoxSerialNumber =
+      stationTemplate?.chargeBoxSerialNumberPrefix &&
+      stationTemplate.chargeBoxSerialNumberPrefix + serialNumberSuffix;
+    stationInfo.meterSerialNumber =
+      stationTemplate?.meterSerialNumberPrefix &&
+      stationTemplate.meterSerialNumberPrefix + serialNumberSuffix;
+  }
+
+  public static propagateSerialNumber(
+    stationTemplate: ChargingStationTemplate,
+    stationInfoSrc: ChargingStationInfo,
+    stationInfoDst: ChargingStationInfo = {} as ChargingStationInfo
+  ) {
+    if (!stationInfoSrc || !stationTemplate) {
+      throw new BaseError(
+        'Missing charging station template or existing configuration to propagate serial number'
+      );
     }
+    stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
+      ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
+      : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
+    stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
+      ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
+      : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
+    stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
+      ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
+      : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
   }
 
   public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
@@ -241,6 +332,7 @@ export class ChargingStationUtils {
     limit: number;
     matchingChargingProfile: ChargingProfile;
   } | null {
+    const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
     for (const chargingProfile of chargingProfiles) {
       // Set helpers
       const currentMoment = moment();
@@ -284,10 +376,7 @@ export class ChargingStationUtils {
               limit: schedulePeriod.limit,
               matchingChargingProfile: chargingProfile,
             };
-            logger.debug(
-              `${logPrefix} Matching charging profile found for power limitation: %j`,
-              result
-            );
+            logger.debug(debugLogMsg, result);
             return result;
           }
           // Find the right schedule period
@@ -301,10 +390,7 @@ export class ChargingStationUtils {
               limit: lastButOneSchedule.limit,
               matchingChargingProfile: chargingProfile,
             };
-            logger.debug(
-              `${logPrefix} Matching charging profile found for power limitation: %j`,
-              result
-            );
+            logger.debug(debugLogMsg, result);
             return result;
           }
           // Keep it
@@ -320,10 +406,7 @@ export class ChargingStationUtils {
               limit: lastButOneSchedule.limit,
               matchingChargingProfile: chargingProfile,
             };
-            logger.debug(
-              `${logPrefix} Matching charging profile found for power limitation: %j`,
-              result
-            );
+            logger.debug(debugLogMsg, result);
             return result;
           }
         }
@@ -337,7 +420,7 @@ export class ChargingStationUtils {
     templateFile: string,
     logPrefix: string
   ): Voltage {
-    const errMsg = `${logPrefix} Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
+    const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
     let defaultVoltageOut: number;
     switch (currentType) {
       case CurrentType.AC:
@@ -347,8 +430,8 @@ export class ChargingStationUtils {
         defaultVoltageOut = Voltage.VOLTAGE_400;
         break;
       default:
-        logger.error(errMsg);
-        throw new Error(errMsg);
+        logger.error(`${logPrefix} ${errMsg}`);
+        throw new BaseError(errMsg);
     }
     return defaultVoltageOut;
   }
@@ -425,15 +508,60 @@ export class ChargingStationUtils {
       }
     }
     if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
-      const errorMsg = `${chargingStation.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
-      logger.error(errorMsg);
-      throw new Error(errorMsg);
+      const errorMsg = `Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
+      logger.error(`${chargingStation.logPrefix()} ${errorMsg}`);
+      throw new BaseError(errorMsg);
     }
     logger.debug(
       `${chargingStation.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
     );
   }
 
+  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)
+      )
+    );
+  }
+
+  public static isRequestCommandSupported(
+    command: RequestCommand,
+    chargingStation: ChargingStation
+  ): boolean {
+    const isRequestCommand = Object.values(RequestCommand).includes(command);
+    if (isRequestCommand && !chargingStation.stationInfo?.commandsSupport?.outgoingCommands) {
+      return true;
+    } else if (isRequestCommand && chargingStation.stationInfo?.commandsSupport?.outgoingCommands) {
+      return chargingStation.stationInfo?.commandsSupport?.outgoingCommands[command] ?? false;
+    }
+    logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`);
+    return false;
+  }
+
+  public static isIncomingRequestCommandSupported(
+    command: IncomingRequestCommand,
+    chargingStation: ChargingStation
+  ): boolean {
+    const isIncomingRequestCommand = Object.values(IncomingRequestCommand).includes(command);
+    if (
+      isIncomingRequestCommand &&
+      !chargingStation.stationInfo?.commandsSupport?.incomingCommands
+    ) {
+      return true;
+    } else if (
+      isIncomingRequestCommand &&
+      chargingStation.stationInfo?.commandsSupport?.incomingCommands
+    ) {
+      return chargingStation.stationInfo?.commandsSupport?.incomingCommands[command] ?? false;
+    }
+    logger.error(`${chargingStation.logPrefix()} Unknown incoming OCPP command '${command}'`);
+    return false;
+  }
+
   private static getRandomSerialNumberSuffix(params?: {
     randomBytesLength?: number;
     upperCase?: boolean;