Ensure start transaction payload is always compliant with OCA specs
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
index da1a8281d62a8a9c286595982361c4bf1e12c0f8..4cf1b386fde85cd580e7b1a6f159bd7aafe4d125 100644 (file)
@@ -1,19 +1,36 @@
-import { ChargingProfile, ChargingSchedulePeriod } from '../types/ocpp/ChargingProfile';
-import { ChargingProfileKindType, RecurrencyKindType } from '../types/ocpp/1.6/ChargingProfile';
-import ChargingStationTemplate, { AmpereUnits } from '../types/ChargingStationTemplate';
+import crypto from 'crypto';
+import path from 'path';
+import { fileURLToPath } from 'url';
 
-import { BootNotificationRequest } from '../types/ocpp/Requests';
-import ChargingStationInfo from '../types/ChargingStationInfo';
+import moment from 'moment';
+
+import BaseError from '../exception/BaseError';
+import type { ChargingStationInfo } from '../types/ChargingStationInfo';
+import {
+  AmpereUnits,
+  type ChargingStationTemplate,
+  CurrentType,
+  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 Utils from '../utils/Utils';
-import { WebSocketCloseEventStatusString } from '../types/WebSocket';
-import { WorkerProcessType } from '../types/Worker';
-import crypto from 'crypto';
 import logger from '../utils/Logger';
-import moment from 'moment';
+import Utils from '../utils/Utils';
+
+const moduleName = 'ChargingStationUtils';
 
 export class ChargingStationUtils {
+  private constructor() {
+    // This is intentional
+  }
+
   public static getChargingStationId(
     index: number,
     stationTemplate: ChargingStationTemplate
@@ -22,7 +39,7 @@ export class ChargingStationUtils {
     const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
     const idSuffix = stationTemplate.nameSuffix ?? '';
     const idStr = '000000000' + index.toString();
-    return stationTemplate.fixedName
+    return stationTemplate?.fixedName
       ? stationTemplate.baseName
       : stationTemplate.baseName +
           '-' +
@@ -31,94 +48,149 @@ export class ChargingStationUtils {
           idSuffix;
   }
 
-  public static getHashId(stationInfo: ChargingStationInfo): string {
-    const hashBootNotificationRequest = {
-      chargePointModel: stationInfo.chargePointModel,
-      chargePointVendor: stationInfo.chargePointVendor,
-      ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumberPrefix) && {
-        chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumberPrefix,
+  public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
+    const chargingStationInfo = {
+      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,
+      // FIXME?: Should a firmware version change always reference a new configuration file?
+      ...(!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(chargingStationInfo) +
+          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(stationTemplate: ChargingStationTemplate): number {
+    let configuredMaxConnectors: number;
+    if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
+      const numberOfConnectors = stationTemplate.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);
+    }
+    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 {
-    return {
-      chargePointModel: stationInfo.chargePointModel,
-      chargePointVendor: stationInfo.chargePointVendor,
-      ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
-        chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
-      }),
-      ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
-        chargePointSerialNumber: stationInfo.chargePointSerialNumber,
-      }),
-      ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
-        firmwareVersion: stationInfo.firmwareVersion,
-      }),
-      ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
-      ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
-      ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
-        meterSerialNumber: stationInfo.meterSerialNumber,
-      }),
-      ...(!Utils.isUndefined(stationInfo.meterType) && {
-        meterType: stationInfo.meterType,
-      }),
-    };
+    const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
+    switch (ocppVersion) {
+      case OCPPVersion.VERSION_16:
+        return {
+          chargePointModel: stationInfo.chargePointModel,
+          chargePointVendor: stationInfo.chargePointVendor,
+          ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
+            chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
+          }),
+          ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
+            chargePointSerialNumber: stationInfo.chargePointSerialNumber,
+          }),
+          ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
+            firmwareVersion: stationInfo.firmwareVersion,
+          }),
+          ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
+          ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
+          ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
+            meterSerialNumber: stationInfo.meterSerialNumber,
+          }),
+          ...(!Utils.isUndefined(stationInfo.meterType) && {
+            meterType: stationInfo.meterType,
+          }),
+        } as OCPP16BootNotificationRequest;
+      case OCPPVersion.VERSION_20:
+      case OCPPVersion.VERSION_201:
+        return {
+          reason: BootReasonEnumType.PowerUp,
+          chargingStation: {
+            model: stationInfo.chargePointModel,
+            vendorName: stationInfo.chargePointVendor,
+            ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
+              firmwareVersion: stationInfo.firmwareVersion,
+            }),
+            ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
+              serialNumber: stationInfo.chargeBoxSerialNumber,
+            }),
+            ...((!Utils.isUndefined(stationInfo.iccid) || !Utils.isUndefined(stationInfo.imsi)) && {
+              modem: {
+                ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
+                ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
+              },
+            }),
+          },
+        } as OCPP20BootNotificationRequest;
+    }
   }
 
   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;
-  }
-
-  /**
-   * Convert websocket error code to human readable string message
-   *
-   * @param code websocket error code
-   * @returns human readable string message
-   */
-  public static getWebSocketCloseEventStatusString(code: number): string {
-    if (code >= 0 && code <= 999) {
-      return '(Unused)';
-    } else if (code >= 1016) {
-      if (code <= 1999) {
-        return '(For WebSocket standard)';
-      } else if (code <= 2999) {
-        return '(For WebSocket extensions)';
-      } else if (code <= 3999) {
-        return '(For libraries and frameworks)';
-      } else if (code <= 4999) {
-        return '(For applications)';
-      }
-    }
-    if (!Utils.isUndefined(WebSocketCloseEventStatusString[code])) {
-      return WebSocketCloseEventStatusString[code] as string;
-    }
-    return '(Unknown)';
+    return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
   }
 
   public static warnDeprecatedTemplateKey(
@@ -129,7 +201,6 @@ export class ChargingStationUtils {
     logMsgToAppend = ''
   ): void {
     if (!Utils.isUndefined(template[key])) {
-      // const logPrefixStr = ` ${chargingStationId} |`;
       logger.warn(
         `${logPrefix} Deprecated template key '${key}' usage in file '${templateFile}'${
           logMsgToAppend && '. ' + logMsgToAppend
@@ -149,30 +220,35 @@ export class ChargingStationUtils {
     }
   }
 
-  public static createStationInfoHash(stationInfo: ChargingStationInfo): ChargingStationInfo {
-    if (!Utils.isEmptyObject(stationInfo)) {
-      const previousInfoHash = stationInfo?.infoHash ?? '';
-      delete stationInfo.infoHash;
-      const currentInfoHash = 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 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;
+    stationInfo.infoHash = crypto
+      .createHash(Constants.DEFAULT_HASH_ALGORITHM)
+      .update(JSON.stringify(stationInfo))
+      .digest('hex');
   }
 
   public static createSerialNumber(
+    stationTemplate: ChargingStationTemplate,
     stationInfo: ChargingStationInfo,
-    existingStationInfo?: ChargingStationInfo,
-    params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean } = {
+    params: {
+      randomSerialNumberUpperCase?: boolean;
+      randomSerialNumber?: boolean;
+    } = {
       randomSerialNumberUpperCase: true,
       randomSerialNumber: true,
     }
@@ -180,29 +256,41 @@ export class ChargingStationUtils {
     params = params ?? {};
     params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
     params.randomSerialNumber = params?.randomSerialNumber ?? true;
-    if (!Utils.isEmptyObject(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
+  ) {
+    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 {
@@ -224,9 +312,9 @@ export class ChargingStationUtils {
   /**
    * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
    *
-   * @param {ChargingProfile[]} chargingProfiles
-   * @param {string} logPrefix
-   * @returns {{ limit, matchingChargingProfile }}
+   * @param chargingProfiles -
+   * @param logPrefix -
+   * @returns
    */
   public static getLimitFromChargingProfiles(
     chargingProfiles: ChargingProfile[],
@@ -235,6 +323,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();
@@ -278,10 +367,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
@@ -295,10 +381,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
@@ -314,10 +397,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;
           }
         }
@@ -326,6 +406,38 @@ 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;