]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ocpp): consolidate MeterValue builders into shared core
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 1 Apr 2026 23:50:56 +0000 (01:50 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 1 Apr 2026 23:50:56 +0000 (01:50 +0200)
Merge buildOCPP16MeterValue and buildOCPP20MeterValue into
buildMeterValue, eliminating ~250 lines of duplicated logic.
Version differences resolved via switch: evseId + sampled
value builder callback.

src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts
src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts
src/charging-station/ocpp/OCPPServiceUtils.ts

index 094915027b16b2406c9df13836459def724c3555..f0f63d31ed165a7b3fd7f6cdaed31963dc630e7c 100644 (file)
@@ -1,41 +1,12 @@
-import type { ChargingStation } from '../../../charging-station/index.js'
-
-import { OCPPError } from '../../../exception/index.js'
 import {
   type ChargingStationInfo,
-  type ConfigurationKeyType,
-  CurrentType,
-  ErrorType,
-  type MeasurandPerPhaseSampledValueTemplates,
-  type MeasurandValues,
   type MeterValueContext,
   type MeterValuePhase,
-  MeterValueUnit,
   type OCPP16BootNotificationRequest,
-  type OCPP16MeterValue,
   type OCPP16SampledValue,
-  RequestCommand,
   type SampledValueTemplate,
 } from '../../../types/index.js'
-import { ACElectricUtils, DCElectricUtils, roundTo } from '../../../utils/index.js'
-import {
-  addLineToLineVoltageToMeterValue,
-  addMainVoltageToMeterValue,
-  addPhaseVoltageToMeterValue,
-  buildCurrentMeasurandValue,
-  buildEmptyMeterValue,
-  buildEnergyMeasurandValue,
-  buildPowerMeasurandValue,
-  buildSocMeasurandValue,
-  buildVoltageMeasurandValue,
-  resolveSampledValueFields,
-  updateConnectorEnergyValues,
-  validateCurrentMeasurandPhaseValue,
-  validateCurrentMeasurandValue,
-  validateEnergyMeasurandValue,
-  validatePowerMeasurandValue,
-  validateSocMeasurandValue,
-} from '../OCPPServiceUtils.js'
+import { resolveSampledValueFields } from '../OCPPServiceUtils.js'
 
 export const buildOCPP16BootNotificationRequest = (
   stationInfo: ChargingStationInfo
@@ -61,263 +32,6 @@ export const buildOCPP16BootNotificationRequest = (
   }),
 })
 
-export const buildOCPP16MeterValue = (
-  chargingStation: ChargingStation,
-  transactionId: number | string,
-  interval: number,
-  measurandsKey?: ConfigurationKeyType,
-  context?: MeterValueContext,
-  debug = false
-): OCPP16MeterValue => {
-  const connectorId = chargingStation.getConnectorIdByTransactionId(transactionId)
-  if (connectorId == null) {
-    throw new OCPPError(
-      ErrorType.INTERNAL_ERROR,
-      `Cannot build MeterValues: no connector found for transaction ${String(transactionId)}`,
-      RequestCommand.METER_VALUES
-    )
-  }
-  const connectorStatus = chargingStation.getConnectorStatus(connectorId)
-  const meterValue = buildEmptyMeterValue() as OCPP16MeterValue
-  const buildVersionedSampledValue = (
-    sampledValueTemplate: SampledValueTemplate,
-    value: number,
-    context?: MeterValueContext,
-    phase?: MeterValuePhase
-  ): OCPP16SampledValue => {
-    return buildOCPP16SampledValue(sampledValueTemplate, value, context, phase)
-  }
-  // SoC measurand
-  const socMeasurand = buildSocMeasurandValue(
-    chargingStation,
-    connectorId,
-    undefined,
-    measurandsKey
-  )
-  if (socMeasurand != null) {
-    const socSampledValue = buildVersionedSampledValue(
-      socMeasurand.template,
-      socMeasurand.value,
-      context
-    )
-    meterValue.sampledValue.push(socSampledValue)
-    validateSocMeasurandValue(
-      chargingStation,
-      connectorId,
-      socSampledValue,
-      socMeasurand.template.minimumValue ?? 0,
-      100,
-      debug
-    )
-  }
-  // Voltage measurand
-  const voltageMeasurand = buildVoltageMeasurandValue(
-    chargingStation,
-    connectorId,
-    undefined,
-    measurandsKey
-  )
-  if (voltageMeasurand != null) {
-    addMainVoltageToMeterValue(
-      chargingStation,
-      meterValue,
-      voltageMeasurand,
-      buildVersionedSampledValue,
-      context
-    )
-    for (
-      let phase = 1;
-      chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
-      phase++
-    ) {
-      addPhaseVoltageToMeterValue(
-        chargingStation,
-        connectorId,
-        meterValue,
-        voltageMeasurand,
-        phase,
-        buildVersionedSampledValue,
-        measurandsKey,
-        context
-      )
-      addLineToLineVoltageToMeterValue(
-        chargingStation,
-        connectorId,
-        meterValue,
-        voltageMeasurand,
-        phase,
-        buildVersionedSampledValue,
-        measurandsKey,
-        context
-      )
-    }
-  }
-  // Power.Active.Import measurand
-  const powerMeasurand = buildPowerMeasurandValue(
-    chargingStation,
-    connectorId,
-    undefined,
-    measurandsKey
-  )
-  if (powerMeasurand?.values.allPhases != null) {
-    const unitDivider = powerMeasurand.template.unit === MeterValueUnit.KILO_WATT ? 1000 : 1
-    const connectorMaximumAvailablePower =
-      chargingStation.getConnectorMaximumAvailablePower(connectorId)
-    const connectorMaximumPower = Math.round(connectorMaximumAvailablePower)
-    const connectorMinimumPower = Math.round(powerMeasurand.template.minimumValue ?? 0)
-
-    meterValue.sampledValue.push(
-      buildVersionedSampledValue(powerMeasurand.template, powerMeasurand.values.allPhases, context)
-    )
-    const sampledValuesIndex = meterValue.sampledValue.length - 1
-    validatePowerMeasurandValue(
-      chargingStation,
-      connectorId,
-      connectorStatus,
-      meterValue.sampledValue[sampledValuesIndex],
-      connectorMaximumPower / unitDivider,
-      connectorMinimumPower / unitDivider,
-      debug
-    )
-    if (chargingStation.getNumberOfPhases() === 3) {
-      const connectorMaximumPowerPerPhase = Math.round(
-        connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
-      )
-      const connectorMinimumPowerPerPhase = Math.round(
-        connectorMinimumPower / chargingStation.getNumberOfPhases()
-      )
-      for (let phase = 1; phase <= chargingStation.getNumberOfPhases(); phase++) {
-        const phaseTemplate =
-          powerMeasurand.perPhaseTemplates[
-            `L${phase.toString()}` as keyof MeasurandPerPhaseSampledValueTemplates
-          ]
-        if (phaseTemplate != null) {
-          const phaseValue = `L${phase.toString()}-N` as MeterValuePhase
-          const phasePowerValue =
-            powerMeasurand.values[`L${phase.toString()}` as keyof MeasurandValues]
-          meterValue.sampledValue.push(
-            buildVersionedSampledValue(phaseTemplate, phasePowerValue, context, phaseValue)
-          )
-          const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
-          validatePowerMeasurandValue(
-            chargingStation,
-            connectorId,
-            connectorStatus,
-            meterValue.sampledValue[sampledValuesPerPhaseIndex],
-            connectorMaximumPowerPerPhase / unitDivider,
-            connectorMinimumPowerPerPhase / unitDivider,
-            debug
-          )
-        }
-      }
-    }
-  }
-  // Current.Import measurand
-  const currentMeasurand = buildCurrentMeasurandValue(
-    chargingStation,
-    connectorId,
-    undefined,
-    measurandsKey
-  )
-  if (currentMeasurand?.values.allPhases != null) {
-    const connectorMaximumAvailablePower =
-      chargingStation.getConnectorMaximumAvailablePower(connectorId)
-    const connectorMaximumAmperage =
-      chargingStation.stationInfo?.currentOutType === CurrentType.AC
-        ? ACElectricUtils.amperagePerPhaseFromPower(
-          chargingStation.getNumberOfPhases(),
-          connectorMaximumAvailablePower,
-          chargingStation.getVoltageOut()
-        )
-        : DCElectricUtils.amperage(connectorMaximumAvailablePower, chargingStation.getVoltageOut())
-    const connectorMinimumAmperage = currentMeasurand.template.minimumValue ?? 0
-
-    meterValue.sampledValue.push(
-      buildVersionedSampledValue(
-        currentMeasurand.template,
-        currentMeasurand.values.allPhases,
-        context
-      )
-    )
-    const sampledValuesIndex = meterValue.sampledValue.length - 1
-    validateCurrentMeasurandValue(
-      chargingStation,
-      connectorId,
-      connectorStatus,
-      meterValue.sampledValue[sampledValuesIndex],
-      connectorMaximumAmperage,
-      connectorMinimumAmperage,
-      debug
-    )
-    for (
-      let phase = 1;
-      chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
-      phase++
-    ) {
-      const phaseValue = `L${phase.toString()}` as MeterValuePhase
-      meterValue.sampledValue.push(
-        buildVersionedSampledValue(
-          currentMeasurand.perPhaseTemplates[
-            phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
-          ] ?? currentMeasurand.template,
-          currentMeasurand.values[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
-          context,
-          phaseValue
-        )
-      )
-      const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
-      validateCurrentMeasurandPhaseValue(
-        chargingStation,
-        connectorId,
-        connectorStatus,
-        meterValue.sampledValue[sampledValuesPerPhaseIndex],
-        connectorMaximumAmperage,
-        connectorMinimumAmperage,
-        debug
-      )
-    }
-  }
-  // Energy.Active.Import.Register measurand (default)
-  const energyMeasurand = buildEnergyMeasurandValue(
-    chargingStation,
-    connectorId,
-    interval,
-    undefined,
-    measurandsKey
-  )
-  if (energyMeasurand != null) {
-    updateConnectorEnergyValues(connectorStatus, energyMeasurand.value)
-    const unitDivider = energyMeasurand.template.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
-    const energySampledValue = buildVersionedSampledValue(
-      energyMeasurand.template,
-      roundTo(
-        chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) / unitDivider,
-        2
-      ),
-      context
-    )
-    meterValue.sampledValue.push(energySampledValue)
-    const connectorMaximumAvailablePower =
-      chargingStation.getConnectorMaximumAvailablePower(connectorId)
-    const connectorMaximumEnergyRounded = roundTo(
-      (connectorMaximumAvailablePower * interval) / (3600 * 1000),
-      2
-    )
-    const connectorMinimumEnergyRounded = roundTo(energyMeasurand.template.minimumValue ?? 0, 2)
-    validateEnergyMeasurandValue(
-      chargingStation,
-      connectorId,
-      energySampledValue,
-      energyMeasurand.value,
-      connectorMinimumEnergyRounded,
-      connectorMaximumEnergyRounded,
-      interval,
-      debug
-    )
-  }
-  return meterValue
-}
-
 /**
  * Builds an OCPP 1.6 sampled value from a template and measurement data.
  * @param sampledValueTemplate - The sampled value template to use.
index 274108f625b6cfad7a548029ca52d4d82b8cde3e..4033bff504539fde6e47fa9db89c151b9d58ad57 100644 (file)
@@ -1,46 +1,18 @@
-import type { ChargingStation } from '../../../charging-station/index.js'
 import type { StopTransactionReason } from '../../../types/index.js'
 
-import { OCPPError } from '../../../exception/index.js'
 import {
   BootReasonEnumType,
   type ChargingStationInfo,
-  type ConfigurationKeyType,
-  CurrentType,
-  ErrorType,
-  type MeasurandPerPhaseSampledValueTemplates,
-  type MeasurandValues,
   type MeterValueContext,
   type MeterValuePhase,
-  MeterValueUnit,
   OCPP16StopTransactionReason,
   type OCPP20BootNotificationRequest,
-  type OCPP20MeterValue,
   OCPP20ReasonEnumType,
   type OCPP20SampledValue,
   OCPP20TriggerReasonEnumType,
-  RequestCommand,
   type SampledValueTemplate,
 } from '../../../types/index.js'
-import { ACElectricUtils, DCElectricUtils, roundTo } from '../../../utils/index.js'
-import {
-  addLineToLineVoltageToMeterValue,
-  addMainVoltageToMeterValue,
-  addPhaseVoltageToMeterValue,
-  buildCurrentMeasurandValue,
-  buildEmptyMeterValue,
-  buildEnergyMeasurandValue,
-  buildPowerMeasurandValue,
-  buildSocMeasurandValue,
-  buildVoltageMeasurandValue,
-  resolveSampledValueFields,
-  updateConnectorEnergyValues,
-  validateCurrentMeasurandPhaseValue,
-  validateCurrentMeasurandValue,
-  validateEnergyMeasurandValue,
-  validatePowerMeasurandValue,
-  validateSocMeasurandValue,
-} from '../OCPPServiceUtils.js'
+import { resolveSampledValueFields } from '../OCPPServiceUtils.js'
 
 export const buildOCPP20BootNotificationRequest = (
   stationInfo: ChargingStationInfo,
@@ -65,259 +37,6 @@ export const buildOCPP20BootNotificationRequest = (
   reason: bootReason,
 })
 
-export const buildOCPP20MeterValue = (
-  chargingStation: ChargingStation,
-  transactionId: number | string,
-  interval: number,
-  measurandsKey?: ConfigurationKeyType,
-  context?: MeterValueContext,
-  debug = false
-): OCPP20MeterValue => {
-  const connectorId = chargingStation.getConnectorIdByTransactionId(transactionId)
-  const evseId = chargingStation.getEvseIdByTransactionId(transactionId)
-  if (connectorId == null || evseId == null) {
-    throw new OCPPError(
-      ErrorType.INTERNAL_ERROR,
-      `Cannot build MeterValues: no connector/EVSE found for transaction ${String(transactionId)}`,
-      RequestCommand.METER_VALUES
-    )
-  }
-  const connectorStatus = chargingStation.getConnectorStatus(connectorId)
-  const meterValue = buildEmptyMeterValue() as OCPP20MeterValue
-  const buildVersionedSampledValue = (
-    sampledValueTemplate: SampledValueTemplate,
-    value: number,
-    context?: MeterValueContext,
-    phase?: MeterValuePhase
-  ): OCPP20SampledValue => {
-    return buildOCPP20SampledValue(sampledValueTemplate, value, context, phase)
-  }
-  // SoC measurand
-  const socMeasurand = buildSocMeasurandValue(chargingStation, connectorId, evseId, measurandsKey)
-  if (socMeasurand != null) {
-    const socSampledValue = buildVersionedSampledValue(
-      socMeasurand.template,
-      socMeasurand.value,
-      context
-    )
-    meterValue.sampledValue.push(socSampledValue)
-    validateSocMeasurandValue(
-      chargingStation,
-      connectorId,
-      socSampledValue,
-      socMeasurand.template.minimumValue ?? 0,
-      100,
-      debug
-    )
-  }
-  // Voltage measurand
-  const voltageMeasurand = buildVoltageMeasurandValue(
-    chargingStation,
-    connectorId,
-    evseId,
-    measurandsKey
-  )
-  if (voltageMeasurand != null) {
-    addMainVoltageToMeterValue(
-      chargingStation,
-      meterValue,
-      voltageMeasurand,
-      buildVersionedSampledValue,
-      context
-    )
-    for (
-      let phase = 1;
-      chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
-      phase++
-    ) {
-      addPhaseVoltageToMeterValue(
-        chargingStation,
-        connectorId,
-        meterValue,
-        voltageMeasurand,
-        phase,
-        buildVersionedSampledValue,
-        measurandsKey,
-        context
-      )
-      addLineToLineVoltageToMeterValue(
-        chargingStation,
-        connectorId,
-        meterValue,
-        voltageMeasurand,
-        phase,
-        buildVersionedSampledValue,
-        measurandsKey,
-        context
-      )
-    }
-  }
-  // Power.Active.Import measurand
-  const powerMeasurand = buildPowerMeasurandValue(
-    chargingStation,
-    connectorId,
-    evseId,
-    measurandsKey
-  )
-  if (powerMeasurand?.values.allPhases != null) {
-    const unitDivider = powerMeasurand.template.unit === MeterValueUnit.KILO_WATT ? 1000 : 1
-    const connectorMaximumAvailablePower =
-      chargingStation.getConnectorMaximumAvailablePower(connectorId)
-    const connectorMaximumPower = Math.round(connectorMaximumAvailablePower)
-    const connectorMinimumPower = Math.round(powerMeasurand.template.minimumValue ?? 0)
-
-    meterValue.sampledValue.push(
-      buildVersionedSampledValue(powerMeasurand.template, powerMeasurand.values.allPhases, context)
-    )
-    const sampledValuesIndex = meterValue.sampledValue.length - 1
-    validatePowerMeasurandValue(
-      chargingStation,
-      connectorId,
-      connectorStatus,
-      meterValue.sampledValue[sampledValuesIndex],
-      connectorMaximumPower / unitDivider,
-      connectorMinimumPower / unitDivider,
-      debug
-    )
-    if (chargingStation.getNumberOfPhases() === 3) {
-      const connectorMaximumPowerPerPhase = Math.round(
-        connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
-      )
-      const connectorMinimumPowerPerPhase = Math.round(
-        connectorMinimumPower / chargingStation.getNumberOfPhases()
-      )
-      for (let phase = 1; phase <= chargingStation.getNumberOfPhases(); phase++) {
-        const phaseTemplate =
-          powerMeasurand.perPhaseTemplates[
-            `L${phase.toString()}` as keyof MeasurandPerPhaseSampledValueTemplates
-          ]
-        if (phaseTemplate != null) {
-          const phaseValue = `L${phase.toString()}-N` as MeterValuePhase
-          const phasePowerValue =
-            powerMeasurand.values[`L${phase.toString()}` as keyof MeasurandValues]
-          meterValue.sampledValue.push(
-            buildVersionedSampledValue(phaseTemplate, phasePowerValue, context, phaseValue)
-          )
-          const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
-          validatePowerMeasurandValue(
-            chargingStation,
-            connectorId,
-            connectorStatus,
-            meterValue.sampledValue[sampledValuesPerPhaseIndex],
-            connectorMaximumPowerPerPhase / unitDivider,
-            connectorMinimumPowerPerPhase / unitDivider,
-            debug
-          )
-        }
-      }
-    }
-  }
-  // Current.Import measurand
-  const currentMeasurand = buildCurrentMeasurandValue(
-    chargingStation,
-    connectorId,
-    evseId,
-    measurandsKey
-  )
-  if (currentMeasurand?.values.allPhases != null) {
-    const connectorMaximumAvailablePower =
-      chargingStation.getConnectorMaximumAvailablePower(connectorId)
-    const connectorMaximumAmperage =
-      chargingStation.stationInfo?.currentOutType === CurrentType.AC
-        ? ACElectricUtils.amperagePerPhaseFromPower(
-          chargingStation.getNumberOfPhases(),
-          connectorMaximumAvailablePower,
-          chargingStation.getVoltageOut()
-        )
-        : DCElectricUtils.amperage(connectorMaximumAvailablePower, chargingStation.getVoltageOut())
-    const connectorMinimumAmperage = currentMeasurand.template.minimumValue ?? 0
-
-    meterValue.sampledValue.push(
-      buildVersionedSampledValue(
-        currentMeasurand.template,
-        currentMeasurand.values.allPhases,
-        context
-      )
-    )
-    const sampledValuesIndex = meterValue.sampledValue.length - 1
-    validateCurrentMeasurandValue(
-      chargingStation,
-      connectorId,
-      connectorStatus,
-      meterValue.sampledValue[sampledValuesIndex],
-      connectorMaximumAmperage,
-      connectorMinimumAmperage,
-      debug
-    )
-    for (
-      let phase = 1;
-      chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
-      phase++
-    ) {
-      const phaseValue = `L${phase.toString()}` as MeterValuePhase
-      meterValue.sampledValue.push(
-        buildVersionedSampledValue(
-          currentMeasurand.perPhaseTemplates[
-            phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
-          ] ?? currentMeasurand.template,
-          currentMeasurand.values[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
-          context,
-          phaseValue
-        )
-      )
-      const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
-      validateCurrentMeasurandPhaseValue(
-        chargingStation,
-        connectorId,
-        connectorStatus,
-        meterValue.sampledValue[sampledValuesPerPhaseIndex],
-        connectorMaximumAmperage,
-        connectorMinimumAmperage,
-        debug
-      )
-    }
-  }
-  // Energy.Active.Import.Register measurand (default)
-  const energyMeasurand = buildEnergyMeasurandValue(
-    chargingStation,
-    connectorId,
-    interval,
-    evseId,
-    measurandsKey
-  )
-  if (energyMeasurand != null) {
-    updateConnectorEnergyValues(connectorStatus, energyMeasurand.value)
-    const unitDivider = energyMeasurand.template.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
-    const energySampledValue = buildVersionedSampledValue(
-      energyMeasurand.template,
-      roundTo(
-        chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) / unitDivider,
-        2
-      ),
-      context
-    )
-    meterValue.sampledValue.push(energySampledValue)
-    const connectorMaximumAvailablePower =
-      chargingStation.getConnectorMaximumAvailablePower(connectorId)
-    const connectorMaximumEnergyRounded = roundTo(
-      (connectorMaximumAvailablePower * interval) / (3600 * 1000),
-      2
-    )
-    const connectorMinimumEnergyRounded = roundTo(energyMeasurand.template.minimumValue ?? 0, 2)
-    validateEnergyMeasurandValue(
-      chargingStation,
-      connectorId,
-      energySampledValue,
-      energyMeasurand.value,
-      connectorMinimumEnergyRounded,
-      connectorMaximumEnergyRounded,
-      interval,
-      debug
-    )
-  }
-  return meterValue
-}
-
 /**
  * Builds an OCPP 2.0 sampled value from a template and measurement data.
  * @param sampledValueTemplate - The sampled value template to use.
index 4cedebb0203c642bbc79cb24cfaa7edff7c50045..533241cc42a88ea07c11699b614ec0cd3ea455cc 100644 (file)
@@ -54,11 +54,11 @@ import {
 } from '../../utils/index.js'
 import {
   buildOCPP16BootNotificationRequest,
-  buildOCPP16MeterValue,
+  buildOCPP16SampledValue,
 } from './1.6/OCPP16RequestBuilders.js'
 import {
   buildOCPP20BootNotificationRequest,
-  buildOCPP20MeterValue,
+  buildOCPP20SampledValue,
 } from './2.0/OCPP20RequestBuilders.js'
 import { OCPPConstants } from './OCPPConstants.js'
 
@@ -988,26 +988,37 @@ export const buildMeterValue = (
   if (transactionId == null) {
     return buildEmptyMeterValue()
   }
+  const connectorId = chargingStation.getConnectorIdByTransactionId(transactionId)
+  let evseId: number | undefined
+  let buildVersionedSampledValue: (
+    sampledValueTemplate: SampledValueTemplate,
+    value: number,
+    context?: MeterValueContext,
+    phase?: MeterValuePhase
+  ) => SampledValue
   switch (chargingStation.stationInfo?.ocppVersion) {
     case OCPPVersion.VERSION_16:
-      return buildOCPP16MeterValue(
-        chargingStation,
-        transactionId,
-        interval,
-        measurandsKey,
-        context,
-        debug
-      )
+      if (connectorId == null) {
+        throw new OCPPError(
+          ErrorType.INTERNAL_ERROR,
+          `Cannot build MeterValues: no connector found for transaction ${String(transactionId)}`,
+          RequestCommand.METER_VALUES
+        )
+      }
+      buildVersionedSampledValue = buildOCPP16SampledValue
+      break
     case OCPPVersion.VERSION_20:
     case OCPPVersion.VERSION_201:
-      return buildOCPP20MeterValue(
-        chargingStation,
-        transactionId,
-        interval,
-        measurandsKey,
-        context,
-        debug
-      )
+      evseId = chargingStation.getEvseIdByTransactionId(transactionId)
+      if (connectorId == null || evseId == null) {
+        throw new OCPPError(
+          ErrorType.INTERNAL_ERROR,
+          `Cannot build MeterValues: no connector/EVSE found for transaction ${String(transactionId)}`,
+          RequestCommand.METER_VALUES
+        )
+      }
+      buildVersionedSampledValue = buildOCPP20SampledValue
+      break
     default:
       throw new OCPPError(
         ErrorType.INTERNAL_ERROR,
@@ -1016,6 +1027,232 @@ export const buildMeterValue = (
         RequestCommand.METER_VALUES
       )
   }
+  const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+  const meterValue: { sampledValue: SampledValue[]; timestamp: Date } = buildEmptyMeterValue()
+  // SoC measurand
+  const socMeasurand = buildSocMeasurandValue(chargingStation, connectorId, evseId, measurandsKey)
+  if (socMeasurand != null) {
+    const socSampledValue = buildVersionedSampledValue(
+      socMeasurand.template,
+      socMeasurand.value,
+      context
+    )
+    meterValue.sampledValue.push(socSampledValue)
+    validateSocMeasurandValue(
+      chargingStation,
+      connectorId,
+      socSampledValue,
+      socMeasurand.template.minimumValue ?? 0,
+      100,
+      debug
+    )
+  }
+  // Voltage measurand
+  const voltageMeasurand = buildVoltageMeasurandValue(
+    chargingStation,
+    connectorId,
+    evseId,
+    measurandsKey
+  )
+  if (voltageMeasurand != null) {
+    addMainVoltageToMeterValue(
+      chargingStation,
+      meterValue,
+      voltageMeasurand,
+      buildVersionedSampledValue,
+      context
+    )
+    for (
+      let phase = 1;
+      chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
+      phase++
+    ) {
+      addPhaseVoltageToMeterValue(
+        chargingStation,
+        connectorId,
+        meterValue,
+        voltageMeasurand,
+        phase,
+        buildVersionedSampledValue,
+        measurandsKey,
+        context
+      )
+      addLineToLineVoltageToMeterValue(
+        chargingStation,
+        connectorId,
+        meterValue,
+        voltageMeasurand,
+        phase,
+        buildVersionedSampledValue,
+        measurandsKey,
+        context
+      )
+    }
+  }
+  // Power.Active.Import measurand
+  const powerMeasurand = buildPowerMeasurandValue(
+    chargingStation,
+    connectorId,
+    evseId,
+    measurandsKey
+  )
+  if (powerMeasurand?.values.allPhases != null) {
+    const unitDivider = powerMeasurand.template.unit === MeterValueUnit.KILO_WATT ? 1000 : 1
+    const connectorMaximumAvailablePower =
+      chargingStation.getConnectorMaximumAvailablePower(connectorId)
+    const connectorMaximumPower = Math.round(connectorMaximumAvailablePower)
+    const connectorMinimumPower = Math.round(powerMeasurand.template.minimumValue ?? 0)
+
+    meterValue.sampledValue.push(
+      buildVersionedSampledValue(powerMeasurand.template, powerMeasurand.values.allPhases, context)
+    )
+    const sampledValuesIndex = meterValue.sampledValue.length - 1
+    validatePowerMeasurandValue(
+      chargingStation,
+      connectorId,
+      connectorStatus,
+      meterValue.sampledValue[sampledValuesIndex],
+      connectorMaximumPower / unitDivider,
+      connectorMinimumPower / unitDivider,
+      debug
+    )
+    if (chargingStation.getNumberOfPhases() === 3) {
+      const connectorMaximumPowerPerPhase = Math.round(
+        connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
+      )
+      const connectorMinimumPowerPerPhase = Math.round(
+        connectorMinimumPower / chargingStation.getNumberOfPhases()
+      )
+      for (let phase = 1; phase <= chargingStation.getNumberOfPhases(); phase++) {
+        const phaseTemplate =
+          powerMeasurand.perPhaseTemplates[
+            `L${phase.toString()}` as keyof MeasurandPerPhaseSampledValueTemplates
+          ]
+        if (phaseTemplate != null) {
+          const phaseValue = `L${phase.toString()}-N` as MeterValuePhase
+          const phasePowerValue =
+            powerMeasurand.values[`L${phase.toString()}` as keyof MeasurandValues]
+          meterValue.sampledValue.push(
+            buildVersionedSampledValue(phaseTemplate, phasePowerValue, context, phaseValue)
+          )
+          const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
+          validatePowerMeasurandValue(
+            chargingStation,
+            connectorId,
+            connectorStatus,
+            meterValue.sampledValue[sampledValuesPerPhaseIndex],
+            connectorMaximumPowerPerPhase / unitDivider,
+            connectorMinimumPowerPerPhase / unitDivider,
+            debug
+          )
+        }
+      }
+    }
+  }
+  // Current.Import measurand
+  const currentMeasurand = buildCurrentMeasurandValue(
+    chargingStation,
+    connectorId,
+    evseId,
+    measurandsKey
+  )
+  if (currentMeasurand?.values.allPhases != null) {
+    const connectorMaximumAvailablePower =
+      chargingStation.getConnectorMaximumAvailablePower(connectorId)
+    const connectorMaximumAmperage =
+      chargingStation.stationInfo.currentOutType === CurrentType.AC
+        ? ACElectricUtils.amperagePerPhaseFromPower(
+          chargingStation.getNumberOfPhases(),
+          connectorMaximumAvailablePower,
+          chargingStation.getVoltageOut()
+        )
+        : DCElectricUtils.amperage(connectorMaximumAvailablePower, chargingStation.getVoltageOut())
+    const connectorMinimumAmperage = currentMeasurand.template.minimumValue ?? 0
+
+    meterValue.sampledValue.push(
+      buildVersionedSampledValue(
+        currentMeasurand.template,
+        currentMeasurand.values.allPhases,
+        context
+      )
+    )
+    const sampledValuesIndex = meterValue.sampledValue.length - 1
+    validateCurrentMeasurandValue(
+      chargingStation,
+      connectorId,
+      connectorStatus,
+      meterValue.sampledValue[sampledValuesIndex],
+      connectorMaximumAmperage,
+      connectorMinimumAmperage,
+      debug
+    )
+    for (
+      let phase = 1;
+      chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
+      phase++
+    ) {
+      const phaseValue = `L${phase.toString()}` as MeterValuePhase
+      meterValue.sampledValue.push(
+        buildVersionedSampledValue(
+          currentMeasurand.perPhaseTemplates[
+            phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
+          ] ?? currentMeasurand.template,
+          currentMeasurand.values[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
+          context,
+          phaseValue
+        )
+      )
+      const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
+      validateCurrentMeasurandPhaseValue(
+        chargingStation,
+        connectorId,
+        connectorStatus,
+        meterValue.sampledValue[sampledValuesPerPhaseIndex],
+        connectorMaximumAmperage,
+        connectorMinimumAmperage,
+        debug
+      )
+    }
+  }
+  // Energy.Active.Import.Register measurand (default)
+  const energyMeasurand = buildEnergyMeasurandValue(
+    chargingStation,
+    connectorId,
+    interval,
+    evseId,
+    measurandsKey
+  )
+  if (energyMeasurand != null) {
+    updateConnectorEnergyValues(connectorStatus, energyMeasurand.value)
+    const unitDivider = energyMeasurand.template.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
+    const energySampledValue = buildVersionedSampledValue(
+      energyMeasurand.template,
+      roundTo(
+        chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) / unitDivider,
+        2
+      ),
+      context
+    )
+    meterValue.sampledValue.push(energySampledValue)
+    const connectorMaximumAvailablePower =
+      chargingStation.getConnectorMaximumAvailablePower(connectorId)
+    const connectorMaximumEnergyRounded = roundTo(
+      (connectorMaximumAvailablePower * interval) / (3600 * 1000),
+      2
+    )
+    const connectorMinimumEnergyRounded = roundTo(energyMeasurand.template.minimumValue ?? 0, 2)
+    validateEnergyMeasurandValue(
+      chargingStation,
+      connectorId,
+      energySampledValue,
+      energyMeasurand.value,
+      connectorMinimumEnergyRounded,
+      connectorMaximumEnergyRounded,
+      interval,
+      debug
+    )
+  }
+  return meterValue as MeterValue
 }
 
 const checkMeasurandPowerDivider = (