refactor(simulator): factor out common helpers
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
index 43b291be0680777088d9bef662cc1c16ab239999..23381f9ec3dd63e5e47faed0301fc6fecbc01f75 100644 (file)
-// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
+// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
-import { ACElectricUtils, DCElectricUtils } from '../../../utils/ElectricUtils';
-import { CurrentType, Voltage } from '../../../types/ChargingStationTemplate';
-import MeasurandPerPhaseSampledValueTemplates, {
-  SampledValueTemplate,
-} from '../../../types/MeasurandPerPhaseSampledValueTemplates';
+import type { JSONSchemaType } from 'ajv';
+
+import type { ChargingStation } from '../../../charging-station';
+import { OCPPError } from '../../../exception';
 import {
+  CurrentType,
+  ErrorType,
+  type JsonType,
+  type MeasurandPerPhaseSampledValueTemplates,
+  type MeasurandValues,
   MeterValueContext,
   MeterValueLocation,
   MeterValueUnit,
-  OCPP16MeterValue,
+  type OCPP16ChargingProfile,
+  type OCPP16IncomingRequestCommand,
+  type OCPP16MeterValue,
   OCPP16MeterValueMeasurand,
   OCPP16MeterValuePhase,
-  OCPP16SampledValue,
-} from '../../../types/ocpp/1.6/MeterValues';
-
-import type ChargingStation from '../../ChargingStation';
-import Constants from '../../../utils/Constants';
-import { ErrorType } from '../../../types/ocpp/ErrorType';
-import MeasurandValues from '../../../types/MeasurandValues';
-import { OCPP16RequestCommand } from '../../../types/ocpp/1.6/Requests';
-import OCPPError from '../../../exception/OCPPError';
-import Utils from '../../../utils/Utils';
-import logger from '../../../utils/Logger';
+  OCPP16RequestCommand,
+  type OCPP16SampledValue,
+  OCPP16StandardParametersKey,
+  type OCPP16SupportedFeatureProfiles,
+  OCPPVersion,
+  type SampledValueTemplate,
+  Voltage,
+} from '../../../types';
+import { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../../../utils';
+import { OCPPServiceUtils } from '../OCPPServiceUtils';
 
-export class OCPP16ServiceUtils {
-  public static checkMeasurandPowerDivider(
+export class OCPP16ServiceUtils extends OCPPServiceUtils {
+  public static checkFeatureProfile(
     chargingStation: ChargingStation,
-    measurandType: OCPP16MeterValueMeasurand
-  ): void {
-    if (Utils.isUndefined(chargingStation.stationInfo.powerDivider)) {
-      const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${
-        measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-      }: powerDivider is undefined`;
-      logger.error(errMsg);
-      throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
-    } else if (chargingStation.stationInfo?.powerDivider <= 0) {
-      const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${
-        measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-      }: powerDivider have zero or below value ${chargingStation.stationInfo.powerDivider}`;
-      logger.error(errMsg);
-      throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
-    }
-  }
-
-  public static buildSampledValue(
-    sampledValueTemplate: SampledValueTemplate,
-    value: number,
-    context?: MeterValueContext,
-    phase?: OCPP16MeterValuePhase
-  ): OCPP16SampledValue {
-    const sampledValueValue = value ?? sampledValueTemplate?.value ?? null;
-    const sampledValueContext = context ?? sampledValueTemplate?.context ?? null;
-    const sampledValueLocation =
-      sampledValueTemplate?.location ??
-      OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate?.measurand ?? null);
-    const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null;
-    return {
-      ...(!Utils.isNullOrUndefined(sampledValueTemplate.unit) && {
-        unit: sampledValueTemplate.unit,
-      }),
-      ...(!Utils.isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
-      ...(!Utils.isNullOrUndefined(sampledValueTemplate.measurand) && {
-        measurand: sampledValueTemplate.measurand,
-      }),
-      ...(!Utils.isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
-      ...(!Utils.isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }),
-      ...(!Utils.isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
-    };
-  }
-
-  public static getMeasurandDefaultUnit(
-    measurandType: OCPP16MeterValueMeasurand
-  ): MeterValueUnit | undefined {
-    switch (measurandType) {
-      case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
-      case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
-      case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
-        return MeterValueUnit.AMP;
-      case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
-      case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
-        return MeterValueUnit.WATT_HOUR;
-      case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
-      case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
-      case OCPP16MeterValueMeasurand.POWER_OFFERED:
-        return MeterValueUnit.WATT;
-      case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
-        return MeterValueUnit.PERCENT;
-      case OCPP16MeterValueMeasurand.VOLTAGE:
-        return MeterValueUnit.VOLT;
-    }
-  }
-
-  public static getMeasurandDefaultLocation(
-    measurandType: OCPP16MeterValueMeasurand
-  ): MeterValueLocation | undefined {
-    switch (measurandType) {
-      case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
-        return MeterValueLocation.EV;
+    featureProfile: OCPP16SupportedFeatureProfiles,
+    command: OCPP16RequestCommand | OCPP16IncomingRequestCommand
+  ): boolean {
+    if (!chargingStation.hasFeatureProfile(featureProfile)) {
+      logger.warn(
+        `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
+          OCPP16StandardParametersKey.SupportedFeatureProfiles
+        } in configuration`
+      );
+      return false;
     }
+    return true;
   }
 
   public static buildMeterValue(
@@ -109,39 +54,49 @@ export class OCPP16ServiceUtils {
     debug = false
   ): OCPP16MeterValue {
     const meterValue: OCPP16MeterValue = {
-      timestamp: new Date().toISOString(),
+      timestamp: new Date(),
       sampledValue: [],
     };
     const connector = chargingStation.getConnectorStatus(connectorId);
     // SoC measurand
-    const socSampledValueTemplate = chargingStation.getSampledValueTemplate(
+    const socSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+      chargingStation,
       connectorId,
       OCPP16MeterValueMeasurand.STATE_OF_CHARGE
     );
     if (socSampledValueTemplate) {
+      const socMaximumValue = 100;
+      const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0;
       const socSampledValueTemplateValue = socSampledValueTemplate.value
         ? Utils.getRandomFloatFluctuatedRounded(
             parseInt(socSampledValueTemplate.value),
             socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
           )
-        : Utils.getRandomInteger(100);
+        : Utils.getRandomInteger(socMaximumValue, socMinimumValue);
       meterValue.sampledValue.push(
         OCPP16ServiceUtils.buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue)
       );
       const sampledValuesIndex = meterValue.sampledValue.length - 1;
-      if (Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > 100 || debug) {
+      if (
+        Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue ||
+        Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue ||
+        debug
+      ) {
         logger.error(
           `${chargingStation.logPrefix()} MeterValues measurand ${
             meterValue.sampledValue[sampledValuesIndex].measurand ??
             OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-          }: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${
+          }: connector id ${connectorId}, transaction id ${
+            connector?.transactionId
+          }, value: ${socMinimumValue}/${
             meterValue.sampledValue[sampledValuesIndex].value
-          }/100`
+          }/${socMaximumValue}}`
         );
       }
     }
     // Voltage measurand
-    const voltageSampledValueTemplate = chargingStation.getSampledValueTemplate(
+    const voltageSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+      chargingStation,
       connectorId,
       OCPP16MeterValueMeasurand.VOLTAGE
     );
@@ -170,7 +125,8 @@ export class OCPP16ServiceUtils {
       ) {
         const phaseLineToNeutralValue = `L${phase}-N`;
         const voltagePhaseLineToNeutralSampledValueTemplate =
-          chargingStation.getSampledValueTemplate(
+          OCPP16ServiceUtils.getSampledValueTemplate(
+            chargingStation,
             connectorId,
             OCPP16MeterValueMeasurand.VOLTAGE,
             phaseLineToNeutralValue as OCPP16MeterValuePhase
@@ -193,7 +149,7 @@ export class OCPP16ServiceUtils {
           OCPP16ServiceUtils.buildSampledValue(
             voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate,
             voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue,
-            null,
+            undefined,
             phaseLineToNeutralValue as OCPP16MeterValuePhase
           )
         );
@@ -204,7 +160,8 @@ export class OCPP16ServiceUtils {
               : chargingStation.getNumberOfPhases()
           }`;
           const voltagePhaseLineToLineSampledValueTemplate =
-            chargingStation.getSampledValueTemplate(
+            OCPP16ServiceUtils.getSampledValueTemplate(
+              chargingStation,
               connectorId,
               OCPP16MeterValueMeasurand.VOLTAGE,
               phaseLineToLineValue as OCPP16MeterValuePhase
@@ -231,7 +188,7 @@ export class OCPP16ServiceUtils {
             OCPP16ServiceUtils.buildSampledValue(
               voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate,
               voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue,
-              null,
+              undefined,
               phaseLineToLineValue as OCPP16MeterValuePhase
             )
           );
@@ -239,24 +196,28 @@ export class OCPP16ServiceUtils {
       }
     }
     // Power.Active.Import measurand
-    const powerSampledValueTemplate = chargingStation.getSampledValueTemplate(
+    const powerSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+      chargingStation,
       connectorId,
       OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT
     );
     let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
     if (chargingStation.getNumberOfPhases() === 3) {
       powerPerPhaseSampledValueTemplates = {
-        L1: chargingStation.getSampledValueTemplate(
+        L1: OCPP16ServiceUtils.getSampledValueTemplate(
+          chargingStation,
           connectorId,
           OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
           OCPP16MeterValuePhase.L1_N
         ),
-        L2: chargingStation.getSampledValueTemplate(
+        L2: OCPP16ServiceUtils.getSampledValueTemplate(
+          chargingStation,
           connectorId,
           OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
           OCPP16MeterValuePhase.L2_N
         ),
-        L3: chargingStation.getSampledValueTemplate(
+        L3: OCPP16ServiceUtils.getSampledValueTemplate(
+          chargingStation,
           connectorId,
           OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
           OCPP16MeterValuePhase.L3_N
@@ -268,7 +229,7 @@ export class OCPP16ServiceUtils {
         chargingStation,
         powerSampledValueTemplate.measurand
       );
-      const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${
+      const errMsg = `MeterValues measurand ${
         powerSampledValueTemplate.measurand ??
         OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
       }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
@@ -279,12 +240,15 @@ export class OCPP16ServiceUtils {
       } measurand value`;
       const powerMeasurandValues = {} as MeasurandValues;
       const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1;
-      const connectorMaximumPower = Math.round(
-        chargingStation.getConnectorMaximumAvailablePower(connectorId)
-      );
+      const connectorMaximumAvailablePower =
+        chargingStation.getConnectorMaximumAvailablePower(connectorId);
+      const connectorMaximumPower = Math.round(connectorMaximumAvailablePower);
       const connectorMaximumPowerPerPhase = Math.round(
-        chargingStation.getConnectorMaximumAvailablePower(connectorId) /
-          chargingStation.getNumberOfPhases()
+        connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
+      );
+      const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue) ?? 0;
+      const connectorMinimumPowerPerPhase = Math.round(
+        connectorMinimumPower / chargingStation.getNumberOfPhases()
       );
       switch (chargingStation.getCurrentOutType()) {
         case CurrentType.AC:
@@ -292,51 +256,83 @@ export class OCPP16ServiceUtils {
             const defaultFluctuatedPowerPerPhase =
               powerSampledValueTemplate.value &&
               Utils.getRandomFloatFluctuatedRounded(
-                parseInt(powerSampledValueTemplate.value) / chargingStation.getNumberOfPhases(),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  powerSampledValueTemplate.value,
+                  connectorMaximumPower / unitDivider,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ) / chargingStation.getNumberOfPhases(),
                 powerSampledValueTemplate.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               );
             const phase1FluctuatedValue =
               powerPerPhaseSampledValueTemplates?.L1?.value &&
               Utils.getRandomFloatFluctuatedRounded(
-                parseInt(powerPerPhaseSampledValueTemplates.L1.value),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  powerPerPhaseSampledValueTemplates.L1.value,
+                  connectorMaximumPowerPerPhase / unitDivider,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ),
                 powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               );
             const phase2FluctuatedValue =
               powerPerPhaseSampledValueTemplates?.L2?.value &&
               Utils.getRandomFloatFluctuatedRounded(
-                parseInt(powerPerPhaseSampledValueTemplates.L2.value),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  powerPerPhaseSampledValueTemplates.L2.value,
+                  connectorMaximumPowerPerPhase / unitDivider,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ),
                 powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               );
             const phase3FluctuatedValue =
               powerPerPhaseSampledValueTemplates?.L3?.value &&
               Utils.getRandomFloatFluctuatedRounded(
-                parseInt(powerPerPhaseSampledValueTemplates.L3.value),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  powerPerPhaseSampledValueTemplates.L3.value,
+                  connectorMaximumPowerPerPhase / unitDivider,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ),
                 powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               );
             powerMeasurandValues.L1 =
               phase1FluctuatedValue ??
               defaultFluctuatedPowerPerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
+              Utils.getRandomFloatRounded(
+                connectorMaximumPowerPerPhase / unitDivider,
+                connectorMinimumPowerPerPhase / unitDivider
+              );
             powerMeasurandValues.L2 =
               phase2FluctuatedValue ??
               defaultFluctuatedPowerPerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
+              Utils.getRandomFloatRounded(
+                connectorMaximumPowerPerPhase / unitDivider,
+                connectorMinimumPowerPerPhase / unitDivider
+              );
             powerMeasurandValues.L3 =
               phase3FluctuatedValue ??
               defaultFluctuatedPowerPerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
+              Utils.getRandomFloatRounded(
+                connectorMaximumPowerPerPhase / unitDivider,
+                connectorMinimumPowerPerPhase / unitDivider
+              );
           } else {
             powerMeasurandValues.L1 = powerSampledValueTemplate.value
               ? Utils.getRandomFloatFluctuatedRounded(
-                  parseInt(powerSampledValueTemplate.value),
+                  OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                    powerSampledValueTemplate.value,
+                    connectorMaximumPower / unitDivider,
+                    { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                  ),
                   powerSampledValueTemplate.fluctuationPercent ??
                     Constants.DEFAULT_FLUCTUATION_PERCENT
                 )
-              : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider);
+              : Utils.getRandomFloatRounded(
+                  connectorMaximumPower / unitDivider,
+                  connectorMinimumPower / unitDivider
+                );
             powerMeasurandValues.L2 = 0;
             powerMeasurandValues.L3 = 0;
           }
@@ -348,14 +344,21 @@ export class OCPP16ServiceUtils {
         case CurrentType.DC:
           powerMeasurandValues.allPhases = powerSampledValueTemplate.value
             ? Utils.getRandomFloatFluctuatedRounded(
-                parseInt(powerSampledValueTemplate.value),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  powerSampledValueTemplate.value,
+                  connectorMaximumPower / unitDivider,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ),
                 powerSampledValueTemplate.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               )
-            : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider);
+            : Utils.getRandomFloatRounded(
+                connectorMaximumPower / unitDivider,
+                connectorMinimumPower / unitDivider
+              );
           break;
         default:
-          logger.error(errMsg);
+          logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
           throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
       }
       meterValue.sampledValue.push(
@@ -366,16 +369,21 @@ export class OCPP16ServiceUtils {
       );
       const sampledValuesIndex = meterValue.sampledValue.length - 1;
       const connectorMaximumPowerRounded = Utils.roundTo(connectorMaximumPower / unitDivider, 2);
+      const connectorMinimumPowerRounded = Utils.roundTo(connectorMinimumPower / unitDivider, 2);
       if (
         Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
           connectorMaximumPowerRounded ||
+        Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
+          connectorMinimumPowerRounded ||
         debug
       ) {
         logger.error(
           `${chargingStation.logPrefix()} MeterValues measurand ${
             meterValue.sampledValue[sampledValuesIndex].measurand ??
             OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-          }: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${
+          }: connector id ${connectorId}, transaction id ${
+            connector?.transactionId
+          }, value: ${connectorMinimumPowerRounded}/${
             meterValue.sampledValue[sampledValuesIndex].value
           }/${connectorMaximumPowerRounded}`
         );
@@ -391,7 +399,7 @@ export class OCPP16ServiceUtils {
             (powerPerPhaseSampledValueTemplates[`L${phase}`] as SampledValueTemplate) ??
               powerSampledValueTemplate,
             powerMeasurandValues[`L${phase}`] as number,
-            null,
+            undefined,
             phaseValue as OCPP16MeterValuePhase
           )
         );
@@ -400,9 +408,15 @@ export class OCPP16ServiceUtils {
           connectorMaximumPowerPerPhase / unitDivider,
           2
         );
+        const connectorMinimumPowerPerPhaseRounded = Utils.roundTo(
+          connectorMinimumPowerPerPhase / unitDivider,
+          2
+        );
         if (
           Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
             connectorMaximumPowerPerPhaseRounded ||
+          Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
+            connectorMinimumPowerPerPhaseRounded ||
           debug
         ) {
           logger.error(
@@ -411,7 +425,9 @@ export class OCPP16ServiceUtils {
               OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
             }: phase ${
               meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
-            }, connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${
+            }, connector id ${connectorId}, transaction id ${
+              connector?.transactionId
+            }, value: ${connectorMinimumPowerPerPhaseRounded}/${
               meterValue.sampledValue[sampledValuesPerPhaseIndex].value
             }/${connectorMaximumPowerPerPhaseRounded}`
           );
@@ -419,24 +435,28 @@ export class OCPP16ServiceUtils {
       }
     }
     // Current.Import measurand
-    const currentSampledValueTemplate = chargingStation.getSampledValueTemplate(
+    const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+      chargingStation,
       connectorId,
       OCPP16MeterValueMeasurand.CURRENT_IMPORT
     );
     let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
     if (chargingStation.getNumberOfPhases() === 3) {
       currentPerPhaseSampledValueTemplates = {
-        L1: chargingStation.getSampledValueTemplate(
+        L1: OCPP16ServiceUtils.getSampledValueTemplate(
+          chargingStation,
           connectorId,
           OCPP16MeterValueMeasurand.CURRENT_IMPORT,
           OCPP16MeterValuePhase.L1
         ),
-        L2: chargingStation.getSampledValueTemplate(
+        L2: OCPP16ServiceUtils.getSampledValueTemplate(
+          chargingStation,
           connectorId,
           OCPP16MeterValueMeasurand.CURRENT_IMPORT,
           OCPP16MeterValuePhase.L2
         ),
-        L3: chargingStation.getSampledValueTemplate(
+        L3: OCPP16ServiceUtils.getSampledValueTemplate(
+          chargingStation,
           connectorId,
           OCPP16MeterValueMeasurand.CURRENT_IMPORT,
           OCPP16MeterValuePhase.L3
@@ -448,7 +468,7 @@ export class OCPP16ServiceUtils {
         chargingStation,
         currentSampledValueTemplate.measurand
       );
-      const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${
+      const errMsg = `MeterValues measurand ${
         currentSampledValueTemplate.measurand ??
         OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
       }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
@@ -458,63 +478,86 @@ export class OCPP16ServiceUtils {
         OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
       } measurand value`;
       const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
+      const connectorMaximumAvailablePower =
+        chargingStation.getConnectorMaximumAvailablePower(connectorId);
+      const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0;
       let connectorMaximumAmperage: number;
       switch (chargingStation.getCurrentOutType()) {
         case CurrentType.AC:
           connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
             chargingStation.getNumberOfPhases(),
-            chargingStation.getConnectorMaximumAvailablePower(connectorId),
+            connectorMaximumAvailablePower,
             chargingStation.getVoltageOut()
           );
           if (chargingStation.getNumberOfPhases() === 3) {
             const defaultFluctuatedAmperagePerPhase =
               currentSampledValueTemplate.value &&
               Utils.getRandomFloatFluctuatedRounded(
-                parseInt(currentSampledValueTemplate.value),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  currentSampledValueTemplate.value,
+                  connectorMaximumAmperage,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ),
                 currentSampledValueTemplate.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               );
             const phase1FluctuatedValue =
               currentPerPhaseSampledValueTemplates?.L1?.value &&
               Utils.getRandomFloatFluctuatedRounded(
-                parseInt(currentPerPhaseSampledValueTemplates.L1.value),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  currentPerPhaseSampledValueTemplates.L1.value,
+                  connectorMaximumAmperage,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ),
                 currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               );
             const phase2FluctuatedValue =
               currentPerPhaseSampledValueTemplates?.L2?.value &&
               Utils.getRandomFloatFluctuatedRounded(
-                parseInt(currentPerPhaseSampledValueTemplates.L2.value),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  currentPerPhaseSampledValueTemplates.L2.value,
+                  connectorMaximumAmperage,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ),
                 currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               );
             const phase3FluctuatedValue =
               currentPerPhaseSampledValueTemplates?.L3?.value &&
               Utils.getRandomFloatFluctuatedRounded(
-                parseInt(currentPerPhaseSampledValueTemplates.L3.value),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  currentPerPhaseSampledValueTemplates.L3.value,
+                  connectorMaximumAmperage,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ),
                 currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               );
             currentMeasurandValues.L1 =
               phase1FluctuatedValue ??
               defaultFluctuatedAmperagePerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumAmperage);
+              Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
             currentMeasurandValues.L2 =
               phase2FluctuatedValue ??
               defaultFluctuatedAmperagePerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumAmperage);
+              Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
             currentMeasurandValues.L3 =
               phase3FluctuatedValue ??
               defaultFluctuatedAmperagePerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumAmperage);
+              Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
           } else {
             currentMeasurandValues.L1 = currentSampledValueTemplate.value
               ? Utils.getRandomFloatFluctuatedRounded(
-                  parseInt(currentSampledValueTemplate.value),
+                  OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                    currentSampledValueTemplate.value,
+                    connectorMaximumAmperage,
+                    { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                  ),
                   currentSampledValueTemplate.fluctuationPercent ??
                     Constants.DEFAULT_FLUCTUATION_PERCENT
                 )
-              : Utils.getRandomFloatRounded(connectorMaximumAmperage);
+              : Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
             currentMeasurandValues.L2 = 0;
             currentMeasurandValues.L3 = 0;
           }
@@ -526,19 +569,23 @@ export class OCPP16ServiceUtils {
           break;
         case CurrentType.DC:
           connectorMaximumAmperage = DCElectricUtils.amperage(
-            chargingStation.getConnectorMaximumAvailablePower(connectorId),
+            connectorMaximumAvailablePower,
             chargingStation.getVoltageOut()
           );
           currentMeasurandValues.allPhases = currentSampledValueTemplate.value
             ? Utils.getRandomFloatFluctuatedRounded(
-                parseInt(currentSampledValueTemplate.value),
+                OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+                  currentSampledValueTemplate.value,
+                  connectorMaximumAmperage,
+                  { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
+                ),
                 currentSampledValueTemplate.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               )
-            : Utils.getRandomFloatRounded(connectorMaximumAmperage);
+            : Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
           break;
         default:
-          logger.error(errMsg);
+          logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
           throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
       }
       meterValue.sampledValue.push(
@@ -551,13 +598,17 @@ export class OCPP16ServiceUtils {
       if (
         Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
           connectorMaximumAmperage ||
+        Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
+          connectorMinimumAmperage ||
         debug
       ) {
         logger.error(
           `${chargingStation.logPrefix()} MeterValues measurand ${
             meterValue.sampledValue[sampledValuesIndex].measurand ??
             OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-          }: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${
+          }: connector id ${connectorId}, transaction id ${
+            connector?.transactionId
+          }, value: ${connectorMinimumAmperage}/${
             meterValue.sampledValue[sampledValuesIndex].value
           }/${connectorMaximumAmperage}`
         );
@@ -573,7 +624,7 @@ export class OCPP16ServiceUtils {
             (currentPerPhaseSampledValueTemplates[phaseValue] as SampledValueTemplate) ??
               currentSampledValueTemplate,
             currentMeasurandValues[phaseValue] as number,
-            null,
+            undefined,
             phaseValue as OCPP16MeterValuePhase
           )
         );
@@ -581,6 +632,8 @@ export class OCPP16ServiceUtils {
         if (
           Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
             connectorMaximumAmperage ||
+          Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
+            connectorMinimumAmperage ||
           debug
         ) {
           logger.error(
@@ -589,7 +642,9 @@ export class OCPP16ServiceUtils {
               OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
             }: phase ${
               meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
-            }, connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${
+            }, connector id ${connectorId}, transaction id ${
+              connector?.transactionId
+            }, value: ${connectorMinimumAmperage}/${
               meterValue.sampledValue[sampledValuesPerPhaseIndex].value
             }/${connectorMaximumAmperage}`
           );
@@ -597,7 +652,10 @@ export class OCPP16ServiceUtils {
       }
     }
     // Energy.Active.Import.Register measurand (default)
-    const energySampledValueTemplate = chargingStation.getSampledValueTemplate(connectorId);
+    const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+      chargingStation,
+      connectorId
+    );
     if (energySampledValueTemplate) {
       OCPP16ServiceUtils.checkMeasurandPowerDivider(
         chargingStation,
@@ -605,23 +663,32 @@ export class OCPP16ServiceUtils {
       );
       const unitDivider =
         energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
+      const connectorMaximumAvailablePower =
+        chargingStation.getConnectorMaximumAvailablePower(connectorId);
       const connectorMaximumEnergyRounded = Utils.roundTo(
-        (chargingStation.getConnectorMaximumAvailablePower(connectorId) * interval) / (3600 * 1000),
+        (connectorMaximumAvailablePower * interval) / (3600 * 1000),
         2
       );
       const energyValueRounded = energySampledValueTemplate.value
         ? // Cumulate the fluctuated value around the static one
           Utils.getRandomFloatFluctuatedRounded(
-            parseInt(energySampledValueTemplate.value),
+            OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+              energySampledValueTemplate.value,
+              connectorMaximumEnergyRounded,
+              {
+                limitationEnabled: chargingStation.getCustomValueLimitationMeterValues(),
+                unitMultiplier: unitDivider,
+              }
+            ),
             energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
           )
         : Utils.getRandomFloatRounded(connectorMaximumEnergyRounded);
       // Persist previous value on connector
       if (
         connector &&
-        !Utils.isNullOrUndefined(connector.energyActiveImportRegisterValue) &&
+        Utils.isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
         connector.energyActiveImportRegisterValue >= 0 &&
-        !Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) &&
+        Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
         connector.transactionEnergyActiveImportRegisterValue >= 0
       ) {
         connector.energyActiveImportRegisterValue += energyValueRounded;
@@ -646,8 +713,8 @@ export class OCPP16ServiceUtils {
           `${chargingStation.logPrefix()} MeterValues measurand ${
             meterValue.sampledValue[sampledValuesIndex].measurand ??
             OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-          }: connectorId ${connectorId}, transaction ${
-            connector.transactionId
+          }: connector id ${connectorId}, transaction id ${
+            connector?.transactionId
           }, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${Utils.roundTo(
             interval / (3600 * 1000),
             4
@@ -664,16 +731,19 @@ export class OCPP16ServiceUtils {
     meterStart: number
   ): OCPP16MeterValue {
     const meterValue: OCPP16MeterValue = {
-      timestamp: new Date().toISOString(),
+      timestamp: new Date(),
       sampledValue: [],
     };
     // Energy.Active.Import.Register measurand (default)
-    const sampledValueTemplate = chargingStation.getSampledValueTemplate(connectorId);
+    const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+      chargingStation,
+      connectorId
+    );
     const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
     meterValue.sampledValue.push(
       OCPP16ServiceUtils.buildSampledValue(
         sampledValueTemplate,
-        Utils.roundTo(meterStart / unitDivider, 4),
+        Utils.roundTo((meterStart ?? 0) / unitDivider, 4),
         MeterValueContext.TRANSACTION_BEGIN
       )
     );
@@ -686,16 +756,19 @@ export class OCPP16ServiceUtils {
     meterStop: number
   ): OCPP16MeterValue {
     const meterValue: OCPP16MeterValue = {
-      timestamp: new Date().toISOString(),
+      timestamp: new Date(),
       sampledValue: [],
     };
     // Energy.Active.Import.Register measurand (default)
-    const sampledValueTemplate = chargingStation.getSampledValueTemplate(connectorId);
+    const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+      chargingStation,
+      connectorId
+    );
     const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
     meterValue.sampledValue.push(
       OCPP16ServiceUtils.buildSampledValue(
         sampledValueTemplate,
-        Utils.roundTo(meterStop / unitDivider, 4),
+        Utils.roundTo((meterStop ?? 0) / unitDivider, 4),
         MeterValueContext.TRANSACTION_END
       )
     );
@@ -711,4 +784,132 @@ export class OCPP16ServiceUtils {
     meterValues.push(transactionEndMeterValue);
     return meterValues;
   }
+
+  public static setChargingProfile(
+    chargingStation: ChargingStation,
+    connectorId: number,
+    cp: OCPP16ChargingProfile
+  ): void {
+    if (
+      Utils.isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)
+    ) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
+      );
+      chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
+    }
+    if (
+      Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
+    ) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization`
+      );
+      chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
+    }
+    let cpReplaced = false;
+    if (Utils.isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
+      chargingStation
+        .getConnectorStatus(connectorId)
+        ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
+          if (
+            chargingProfile.chargingProfileId === cp.chargingProfileId ||
+            (chargingProfile.stackLevel === cp.stackLevel &&
+              chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
+          ) {
+            chargingStation.getConnectorStatus(connectorId).chargingProfiles[index] = cp;
+            cpReplaced = true;
+          }
+        });
+    }
+    !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
+  }
+
+  public static parseJsonSchemaFile<T extends JsonType>(
+    relativePath: string,
+    moduleName?: string,
+    methodName?: string
+  ): JSONSchemaType<T> {
+    return super.parseJsonSchemaFile<T>(
+      relativePath,
+      OCPPVersion.VERSION_16,
+      moduleName,
+      methodName
+    );
+  }
+
+  private static buildSampledValue(
+    sampledValueTemplate: SampledValueTemplate,
+    value: number,
+    context?: MeterValueContext,
+    phase?: OCPP16MeterValuePhase
+  ): OCPP16SampledValue {
+    const sampledValueValue = value ?? sampledValueTemplate?.value ?? null;
+    const sampledValueContext = context ?? sampledValueTemplate?.context ?? null;
+    const sampledValueLocation =
+      sampledValueTemplate?.location ??
+      OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate?.measurand ?? null);
+    const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null;
+    return {
+      ...(!Utils.isNullOrUndefined(sampledValueTemplate.unit) && {
+        unit: sampledValueTemplate.unit,
+      }),
+      ...(!Utils.isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
+      ...(!Utils.isNullOrUndefined(sampledValueTemplate.measurand) && {
+        measurand: sampledValueTemplate.measurand,
+      }),
+      ...(!Utils.isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
+      ...(!Utils.isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }),
+      ...(!Utils.isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
+    };
+  }
+
+  private static checkMeasurandPowerDivider(
+    chargingStation: ChargingStation,
+    measurandType: OCPP16MeterValueMeasurand
+  ): void {
+    if (Utils.isUndefined(chargingStation.powerDivider)) {
+      const errMsg = `MeterValues measurand ${
+        measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+      }: powerDivider is undefined`;
+      logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
+      throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
+    } else if (chargingStation?.powerDivider <= 0) {
+      const errMsg = `MeterValues measurand ${
+        measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+      }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
+      logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
+      throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
+    }
+  }
+
+  private static getMeasurandDefaultLocation(
+    measurandType: OCPP16MeterValueMeasurand
+  ): MeterValueLocation | undefined {
+    switch (measurandType) {
+      case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
+        return MeterValueLocation.EV;
+    }
+  }
+
+  private static getMeasurandDefaultUnit(
+    measurandType: OCPP16MeterValueMeasurand
+  ): MeterValueUnit | undefined {
+    switch (measurandType) {
+      case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
+      case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
+      case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
+        return MeterValueUnit.AMP;
+      case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
+      case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
+        return MeterValueUnit.WATT_HOUR;
+      case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
+      case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
+      case OCPP16MeterValueMeasurand.POWER_OFFERED:
+        return MeterValueUnit.WATT;
+      case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
+        return MeterValueUnit.PERCENT;
+      case OCPP16MeterValueMeasurand.VOLTAGE:
+        return MeterValueUnit.VOLT;
+    }
+  }
 }