build(simulator): cleanup TS configuration
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
index e378fdec87bd26832ce1abf4ebe2cddd53b8a1b9..ab781fae3cb3382022abb8f61ed7b55f021e9ef0 100644 (file)
@@ -1,34 +1,37 @@
-import crypto from 'crypto';
-import path from 'path';
-import { fileURLToPath } from 'url';
+import { createHash, randomBytes } from 'node:crypto';
+import type { EventEmitter } from 'node:events';
+import { basename, dirname, join } 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 './ChargingStation';
+import { BaseError } from '../exception';
 import {
   AmpereUnits,
+  AvailabilityType,
+  type BootNotificationRequest,
+  BootReasonEnumType,
+  type ChargingProfile,
+  ChargingProfileKindType,
+  ChargingRateUnitType,
+  type ChargingSchedulePeriod,
+  type ChargingStationInfo,
   type ChargingStationTemplate,
+  ChargingStationWorkerMessageEvents,
+  ConnectorPhaseRotation,
+  type ConnectorStatus,
+  ConnectorStatusEnum,
   CurrentType,
+  type EvseTemplate,
+  type OCPP16BootNotificationRequest,
+  type OCPP20BootNotificationRequest,
+  OCPPVersion,
+  RecurrencyKindType,
   Voltage,
-} from '../types/ChargingStationTemplate';
-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 { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues';
-import {
-  type BootNotificationRequest,
-  IncomingRequestCommand,
-  RequestCommand,
-} 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';
-import type ChargingStation from './ChargingStation';
-import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils';
+} from '../types';
+import { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../utils';
 
 const moduleName = 'ChargingStationUtils';
 
@@ -43,19 +46,30 @@ 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 countReservableConnectors(connectors: Map<number, ConnectorStatus>) {
+    let reservableConnectors = 0;
+    for (const [connectorId, connectorStatus] of connectors) {
+      if (connectorId === 0) {
+        continue;
+      }
+      if (connectorStatus.status === ConnectorStatusEnum.Available) {
+        ++reservableConnectors;
+      }
+    }
+    return reservableConnectors;
   }
 
   public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
-    const hashBootNotificationRequest = {
+    const chargingStationInfo = {
       chargePointModel: stationTemplate.chargePointModel,
       chargePointVendor: stationTemplate.chargePointVendor,
       ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
@@ -64,11 +78,6 @@ export class ChargingStationUtils {
       ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
         chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
       }),
-      ...(!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,
       }),
@@ -76,136 +85,311 @@ export class ChargingStationUtils {
         meterType: stationTemplate.meterType,
       }),
     };
-    return crypto
-      .createHash(Constants.DEFAULT_HASH_ALGORITHM)
+    return createHash(Constants.DEFAULT_HASH_ALGORITHM)
       .update(
-        JSON.stringify(hashBootNotificationRequest) +
-          ChargingStationUtils.getChargingStationId(index, stationTemplate)
+        `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
+          index,
+          stationTemplate
+        )}`
       )
       .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 getPhaseRotationValue(
+    connectorId: number,
+    numberOfPhases: number
+  ): string | undefined {
+    // AC/DC
+    if (connectorId === 0 && numberOfPhases === 0) {
+      return `${connectorId}.${ConnectorPhaseRotation.RST}`;
+    } else if (connectorId > 0 && numberOfPhases === 0) {
+      return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
+      // AC
+    } else if (connectorId > 0 && numberOfPhases === 1) {
+      return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
+    } else if (connectorId > 0 && numberOfPhases === 3) {
+      return `${connectorId}.${ConnectorPhaseRotation.RST}`;
+    }
+  }
+
+  public static getMaxNumberOfEvses(evses: Record<string, EvseTemplate>): number {
+    if (!evses) {
       return -1;
     }
-    return Object.keys(templateConnectors).length;
+    return Object.keys(evses).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 getMaxNumberOfConnectors(connectors: Record<string, ConnectorStatus>): number {
+    if (!connectors) {
+      return -1;
     }
+    return Object.keys(connectors).length;
   }
 
-  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;
+  public static getBootConnectorStatus(
+    chargingStation: ChargingStation,
+    connectorId: number,
+    connectorStatus: ConnectorStatus
+  ): ConnectorStatusEnum {
+    let connectorBootStatus: ConnectorStatusEnum;
+    if (
+      !connectorStatus?.status &&
+      (chargingStation.isChargingStationAvailable() === false ||
+        chargingStation.isConnectorAvailable(connectorId) === false)
+    ) {
+      connectorBootStatus = ConnectorStatusEnum.Unavailable;
+    } else if (!connectorStatus?.status && connectorStatus?.bootStatus) {
+      // Set boot status in template at startup
+      connectorBootStatus = connectorStatus?.bootStatus;
+    } else if (connectorStatus?.status) {
+      // Set previous status at startup
+      connectorBootStatus = connectorStatus?.status;
     } else {
-      configuredMaxConnectors = stationTemplate?.Connectors[0]
-        ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
-        : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
+      // Set default status
+      connectorBootStatus = ConnectorStatusEnum.Available;
     }
-    return configuredMaxConnectors;
+    return connectorBootStatus;
   }
 
-  public static checkConfiguredMaxConnectors(
-    configuredMaxConnectors: number,
-    templateFile: string,
-    logPrefix: string
-  ): void {
-    if (configuredMaxConnectors <= 0) {
+  public static checkTemplate(
+    stationTemplate: ChargingStationTemplate,
+    logPrefix: string,
+    templateFile: string
+  ) {
+    if (Utils.isNullOrUndefined(stationTemplate)) {
+      const errorMsg = `Failed to read charging station template file ${templateFile}`;
+      logger.error(`${logPrefix} ${errorMsg}`);
+      throw new BaseError(errorMsg);
+    }
+    if (Utils.isEmptyObject(stationTemplate)) {
+      const errorMsg = `Empty charging station information from template file ${templateFile}`;
+      logger.error(`${logPrefix} ${errorMsg}`);
+      throw new BaseError(errorMsg);
+    }
+    if (Utils.isEmptyObject(stationTemplate.AutomaticTransactionGenerator)) {
+      stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION;
       logger.warn(
-        `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
+        `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
+        Constants.DEFAULT_ATG_CONFIGURATION
+      );
+    }
+    if (
+      Utils.isNullOrUndefined(stationTemplate.idTagsFile) ||
+      Utils.isEmptyString(stationTemplate.idTagsFile)
+    ) {
+      logger.warn(
+        `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
       );
     }
   }
 
-  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,
-      }),
-    };
-  }
-
-  public static workerPoolInUse(): boolean {
-    return [WorkerProcessType.DYNAMIC_POOL, WorkerProcessType.STATIC_POOL].includes(
-      Configuration.getWorker().processType
+  public static checkConnectorsConfiguration(
+    stationTemplate: ChargingStationTemplate,
+    logPrefix: string,
+    templateFile: string
+  ): {
+    configuredMaxConnectors: number;
+    templateMaxConnectors: number;
+    templateMaxAvailableConnectors: number;
+  } {
+    const configuredMaxConnectors =
+      ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
+    ChargingStationUtils.checkConfiguredMaxConnectors(
+      configuredMaxConnectors,
+      logPrefix,
+      templateFile
+    );
+    const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors(
+      stationTemplate.Connectors
     );
+    ChargingStationUtils.checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
+    const templateMaxAvailableConnectors = stationTemplate?.Connectors[0]
+      ? templateMaxConnectors - 1
+      : templateMaxConnectors;
+    if (
+      configuredMaxConnectors > templateMaxAvailableConnectors &&
+      !stationTemplate?.randomConnectors
+    ) {
+      logger.warn(
+        `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`
+      );
+      stationTemplate.randomConnectors = true;
+    }
+    return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors };
   }
 
-  public static workerDynamicPoolInUse(): boolean {
-    return Configuration.getWorker().processType === WorkerProcessType.DYNAMIC_POOL;
+  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 id ${connectorId} status configuration defined, undefine it`
+      );
+      delete connectorStatus.status;
+    }
   }
 
-  public static warnDeprecatedTemplateKey(
-    template: ChargingStationTemplate,
-    key: string,
-    templateFile: string,
+  public static buildConnectorsMap(
+    connectors: Record<string, ConnectorStatus>,
     logPrefix: string,
-    logMsgToAppend = ''
-  ): void {
-    if (!Utils.isUndefined(template[key])) {
+    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} Deprecated template key '${key}' usage in file '${templateFile}'${
-          logMsgToAppend && '. ' + logMsgToAppend
-        }`
+        `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`
       );
     }
+    return connectorsMap;
   }
 
-  public static convertDeprecatedTemplateKey(
-    template: ChargingStationTemplate,
-    deprecatedKey: string,
-    key: string
+  public static initializeConnectorsMapStatus(
+    connectors: Map<number, ConnectorStatus>,
+    logPrefix: string
   ): void {
-    if (!Utils.isUndefined(template[deprecatedKey])) {
-      template[key] = template[deprecatedKey] as unknown;
-      delete template[deprecatedKey];
+    for (const connectorId of connectors.keys()) {
+      if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
+        logger.warn(
+          `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
+            connectors.get(connectorId)?.transactionId
+          }`
+        );
+      }
+      if (connectorId === 0) {
+        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
+  ): BootNotificationRequest {
+    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: bootReason,
+          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 warnTemplateKeysDeprecation(
+    stationTemplate: ChargingStationTemplate,
+    logPrefix: string,
+    templateFile: 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,
+        logPrefix,
+        templateFile,
+        `Use '${templateKey.key}' instead`
+      );
+      ChargingStationUtils.convertDeprecatedTemplateKey(
+        stationTemplate,
+        templateKey.deprecatedKey,
+        templateKey.key
+      );
     }
   }
 
   public static stationTemplateToStationInfo(
     stationTemplate: ChargingStationTemplate
   ): ChargingStationInfo {
-    stationTemplate = Utils.cloneObject(stationTemplate);
+    stationTemplate = Utils.cloneObject<ChargingStationTemplate>(stationTemplate);
     delete stationTemplate.power;
     delete stationTemplate.powerUnit;
+    delete stationTemplate?.Connectors;
+    delete stationTemplate?.Evses;
     delete stationTemplate.Configuration;
     delete stationTemplate.AutomaticTransactionGenerator;
     delete stationTemplate.chargeBoxSerialNumberPrefix;
@@ -214,17 +398,9 @@ export class ChargingStationUtils {
     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 = {} as ChargingStationInfo,
+    stationInfo: ChargingStationInfo,
     params: {
       randomSerialNumberUpperCase?: boolean;
       randomSerialNumber?: boolean;
@@ -233,29 +409,24 @@ export class ChargingStationUtils {
       randomSerialNumber: true,
     }
   ): void {
-    params = params ?? {};
-    params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
-    params.randomSerialNumber = params?.randomSerialNumber ?? true;
+    params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
     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;
+    Utils.isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
+      (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
+    Utils.isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
+      (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
+    Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
+      (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
   }
 
   public static propagateSerialNumber(
     stationTemplate: ChargingStationTemplate,
     stationInfoSrc: ChargingStationInfo,
-    stationInfoDst: ChargingStationInfo = {} as ChargingStationInfo
+    stationInfoDst: ChargingStationInfo
   ) {
     if (!stationInfoSrc || !stationTemplate) {
       throw new BaseError(
@@ -289,49 +460,216 @@ export class ChargingStationUtils {
     return unitDivider;
   }
 
-  public static setChargingProfile(
+  public static getChargingStationConnectorChargingProfilesPowerLimit(
     chargingStation: ChargingStation,
-    connectorId: number,
-    cp: ChargingProfile
+    connectorId: number
+  ): number | undefined {
+    let limit: number, matchingChargingProfile: ChargingProfile;
+    // Get charging profiles for connector and sort by stack level
+    const chargingProfiles =
+      Utils.cloneObject<ChargingProfile[]>(
+        chargingStation.getConnectorStatus(connectorId)?.chargingProfiles
+      )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
+    // Get profiles on connector 0
+    if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
+      chargingProfiles.push(
+        ...Utils.cloneObject<ChargingProfile[]>(
+          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,
+    logPrefix: string,
+    templateFile: string
+  ): Voltage {
+    const errorMsg = `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} ${errorMsg}`);
+        throw new BaseError(errorMsg);
+    }
+    return defaultVoltageOut;
+  }
+
+  public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
+    return (
+      stationInfo.idTagsFile &&
+      join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
+    );
+  }
+
+  public static waitForChargingStationEvents = async (
+    emitter: EventEmitter,
+    event: ChargingStationWorkerMessageEvents,
+    eventsToWait: number
+  ): Promise<number> => {
+    return new Promise((resolve) => {
+      let events = 0;
+      if (eventsToWait === 0) {
+        resolve(events);
+      }
+      emitter.on(event, () => {
+        ++events;
+        if (events === eventsToWait) {
+          resolve(events);
+        }
+      });
+    });
+  };
+
+  private static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
+    let configuredMaxConnectors: number;
+    if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
+      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 if (stationTemplate.Connectors && !stationTemplate.Evses) {
+      configuredMaxConnectors = stationTemplate?.Connectors[0]
+        ? ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
+        : ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors);
+    } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
+      configuredMaxConnectors = 0;
+      for (const evse in stationTemplate.Evses) {
+        if (evse === '0') {
+          continue;
+        }
+        configuredMaxConnectors += ChargingStationUtils.getMaxNumberOfConnectors(
+          stationTemplate.Evses[evse].Connectors
+        );
+      }
+    }
+    return configuredMaxConnectors;
+  }
+
+  private static checkConfiguredMaxConnectors(
+    configuredMaxConnectors: number,
+    logPrefix: string,
+    templateFile: string
   ): void {
-    if (Utils.isNullOrUndefined(chargingStation.getConnectorStatus(connectorId).chargingProfiles)) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to set a charging profile on connectorId ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
+    if (configuredMaxConnectors <= 0) {
+      logger.warn(
+        `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
       );
-      chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
     }
-    if (Array.isArray(chargingStation.getConnectorStatus(connectorId).chargingProfiles) === false) {
+  }
+
+  private static checkTemplateMaxConnectors(
+    templateMaxConnectors: number,
+    logPrefix: string,
+    templateFile: string
+  ): void {
+    if (templateMaxConnectors === 0) {
+      logger.warn(
+        `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
+      );
+    } else if (templateMaxConnectors < 0) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to set a charging profile on connectorId ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization`
+        `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
       );
-      chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
     }
-    let cpReplaced = false;
-    if (!Utils.isEmptyArray(chargingStation.getConnectorStatus(connectorId).chargingProfiles)) {
-      chargingStation
-        .getConnectorStatus(connectorId)
-        .chargingProfiles?.forEach((chargingProfile: ChargingProfile, index: number) => {
-          if (
-            chargingProfile.chargingProfileId === cp.chargingProfileId ||
-            (chargingProfile.stackLevel === cp.stackLevel &&
-              chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
-          ) {
-            chargingStation.getConnectorStatus(connectorId).chargingProfiles[index] = cp;
-            cpReplaced = true;
-          }
-        });
+  }
+
+  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,
+    logPrefix: string,
+    templateFile: 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];
     }
-    !cpReplaced && chargingStation.getConnectorStatus(connectorId).chargingProfiles?.push(cp);
   }
 
   /**
-   * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
+   * Charging profiles should already be sorted by connector id 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(
+  private static getLimitFromChargingProfiles(
     chargingProfiles: ChargingProfile[],
     logPrefix: string
   ): {
@@ -339,10 +677,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 (
@@ -350,8 +694,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(),
@@ -421,166 +769,11 @@ 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 getSampledValueTemplate(
-    chargingStation: ChargingStation,
-    connectorId: number,
-    measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
-    phase?: MeterValuePhase
-  ): SampledValueTemplate | undefined {
-    const onPhaseStr = phase ? `on phase ${phase} ` : '';
-    if (Constants.SUPPORTED_MEASURANDS.includes(measurand) === false) {
-      logger.warn(
-        `${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
-      );
-      return;
-    }
-    if (
-      measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
-      !ChargingStationConfigurationUtils.getConfigurationKey(
-        chargingStation,
-        StandardParametersKey.MeterValuesSampledData
-      )?.value.includes(measurand)
-    ) {
-      logger.debug(
-        `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
-          StandardParametersKey.MeterValuesSampledData
-        }' OCPP parameter`
-      );
-      return;
-    }
-    const sampledValueTemplates: SampledValueTemplate[] =
-      chargingStation.getConnectorStatus(connectorId).MeterValues;
-    for (
-      let index = 0;
-      !Utils.isEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length;
-      index++
-    ) {
-      if (
-        Constants.SUPPORTED_MEASURANDS.includes(
-          sampledValueTemplates[index]?.measurand ??
-            MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-        ) === false
-      ) {
-        logger.warn(
-          `${chargingStation.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
-        );
-      } else if (
-        phase &&
-        sampledValueTemplates[index]?.phase === phase &&
-        sampledValueTemplates[index]?.measurand === measurand &&
-        ChargingStationConfigurationUtils.getConfigurationKey(
-          chargingStation,
-          StandardParametersKey.MeterValuesSampledData
-        )?.value.includes(measurand) === true
-      ) {
-        return sampledValueTemplates[index];
-      } else if (
-        !phase &&
-        !sampledValueTemplates[index].phase &&
-        sampledValueTemplates[index]?.measurand === measurand &&
-        ChargingStationConfigurationUtils.getConfigurationKey(
-          chargingStation,
-          StandardParametersKey.MeterValuesSampledData
-        )?.value.includes(measurand) === true
-      ) {
-        return sampledValueTemplates[index];
-      } else if (
-        measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
-        (!sampledValueTemplates[index].measurand ||
-          sampledValueTemplates[index].measurand === measurand)
-      ) {
-        return sampledValueTemplates[index];
-      }
-    }
-    if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
-      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 === true &&
-      !chargingStation.stationInfo?.commandsSupport?.outgoingCommands
-    ) {
-      return true;
-    } else if (
-      isRequestCommand === true &&
-      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 === true &&
-      !chargingStation.stationInfo?.commandsSupport?.incomingCommands
-    ) {
-      return true;
-    } else if (
-      isIncomingRequestCommand === true &&
-      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;
   }): string {
-    const randomSerialNumberSuffix = crypto
-      .randomBytes(params?.randomBytesLength ?? 16)
-      .toString('hex');
+    const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
     if (params?.upperCase) {
       return randomSerialNumberSuffix.toUpperCase();
     }