]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
feat: support configurable measurands per transaction stage in OCPP 2.0
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 27 Mar 2026 23:13:03 +0000 (00:13 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 27 Mar 2026 23:13:03 +0000 (00:13 +0100)
- Thread measurandsKey and context params through the meter value pipeline
  (getSampledValueTemplate, build*MeasurandValue, voltage helpers)
- buildTransactionStartedMeterValues uses SampledDataCtrlr.TxStartedMeasurands
  with Transaction.Begin context
- buildTransactionEndedMeterValues uses SampledDataCtrlr.TxEndedMeasurands
  with Transaction.End context
- Add OCPP 1.6→2.0 mappings for MeterValuesAlignedData,
  ClockAlignedDataInterval, StopTxnSampledData, StopTxnAlignedData
- Reorder getSampledValueTemplate params: measurandsKey before measurand
- Log warn (not debug) when meter value building fails in transaction events
- Add .trim() to convertToBoolean for whitespace-padded values

src/charging-station/ConfigurationKeyUtils.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
src/utils/Utils.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStopTransaction.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts
ui/web/src/composables/Utils.ts

index 86c934d9ff452f3cf53cc8effce3f68ea06a7c5f..49a9d80ead63ec8ba45a3ca2aa050f102216bb6d 100644 (file)
@@ -79,6 +79,31 @@ const OCPP2_PARAMETER_KEY_MAP = new Map<
               StandardParametersKey.WebSocketPingInterval
             ),
           ],
+          [
+            StandardParametersKey.MeterValuesAlignedData,
+            buildConfigKey(OCPP20ComponentName.AlignedDataCtrlr, 'Measurands'),
+          ],
+          [
+            StandardParametersKey.ClockAlignedDataInterval,
+            buildConfigKey(
+              OCPP20ComponentName.AlignedDataCtrlr,
+              StandardParametersKey.AlignedDataInterval
+            ),
+          ],
+          [
+            StandardParametersKey.StopTxnSampledData,
+            buildConfigKey(
+              OCPP20ComponentName.SampledDataCtrlr,
+              StandardParametersKey.TxEndedMeasurands
+            ),
+          ],
+          [
+            StandardParametersKey.StopTxnAlignedData,
+            buildConfigKey(
+              OCPP20ComponentName.AlignedDataCtrlr,
+              StandardParametersKey.TxEndedMeasurands
+            ),
+          ],
         ] as [ConfigurationKeyType, ConfigurationKeyType][]
       ).map(([from, to]) => [
         from,
index 1146fb44c14d7726df5f5bfbd6e5d193acd277b4..d11e88f35b866507f541cd998fae148946de17b7 100644 (file)
@@ -405,11 +405,10 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
         if (response.status === RequestStartStopStatusEnumType.Accepted) {
           const connectorId = chargingStation.getConnectorIdByTransactionId(response.transactionId)
           if (connectorId != null && response.transactionId != null) {
-            const connectorStatus = chargingStation.getConnectorStatus(connectorId)
-            const startedMeterValues =
-              connectorStatus != null
-                ? OCPP20ServiceUtils.buildTransactionStartedMeterValues(connectorStatus)
-                : []
+            const startedMeterValues = OCPP20ServiceUtils.buildTransactionStartedMeterValues(
+              chargingStation,
+              response.transactionId
+            )
             OCPP20ServiceUtils.sendTransactionEvent(
               chargingStation,
               OCPP20TransactionEventEnumType.Started,
index f2e4d29f60a9760ec9c1e809a77509f1a0ffdda0..d15b038dbd69c677e4ba323879b360e78f608eba 100644 (file)
@@ -10,7 +10,6 @@ import {
   OCPP20ComponentName,
   type OCPP20EVSEType,
   OCPP20IncomingRequestCommand,
-  OCPP20MeasurandEnumType,
   type OCPP20MeterValue,
   OCPP20OptionalVariableName,
   OCPP20ReadingContextEnumType,
@@ -26,6 +25,7 @@ import {
   OCPP20TriggerReasonEnumType,
   OCPPVersion,
   ReasonCodeEnumType,
+  StandardParametersKey,
   type UUIDv4,
 } from '../../../types/index.js'
 import {
@@ -99,11 +99,30 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils {
     [OCPP20RequestCommand.TRANSACTION_EVENT, 'TransactionEvent'],
   ]
 
-  static buildTransactionStartedMeterValues (connectorStatus: ConnectorStatus): OCPP20MeterValue[] {
-    return OCPP20ServiceUtils.buildEnergyMeterValues(
-      connectorStatus,
-      OCPP20ReadingContextEnumType.TRANSACTION_BEGIN
-    )
+  static buildTransactionStartedMeterValues (
+    chargingStation: ChargingStation,
+    transactionId: number | string
+  ): OCPP20MeterValue[] {
+    try {
+      const measurandsKey = buildConfigKey(
+        OCPP20ComponentName.SampledDataCtrlr,
+        StandardParametersKey.TxStartedMeasurands
+      )
+      const meterValue = buildMeterValue(
+        chargingStation,
+        transactionId,
+        0,
+        false,
+        measurandsKey,
+        OCPP20ReadingContextEnumType.TRANSACTION_BEGIN
+      ) as OCPP20MeterValue
+      return meterValue.sampledValue.length > 0 ? [meterValue] : []
+    } catch (error) {
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.buildTransactionStartedMeterValues: ${(error as Error).message}`
+      )
+      return []
+    }
   }
 
   public static async cleanupEndedTransaction (
@@ -720,34 +739,30 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils {
     }
   }
 
-  private static buildEnergyMeterValues (
-    connectorStatus: ConnectorStatus,
-    context: OCPP20ReadingContextEnumType
-  ): OCPP20MeterValue[] {
-    const meterValues: OCPP20MeterValue[] = []
-    const energyValue = connectorStatus.transactionEnergyActiveImportRegisterValue ?? 0
-    if (energyValue >= 0) {
-      meterValues.push({
-        sampledValue: [
-          {
-            context,
-            measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
-            value: energyValue,
-          },
-        ],
-        timestamp: new Date(),
-      })
-    }
-    return meterValues
-  }
-
   private static buildTransactionEndedMeterValues (
-    connectorStatus: ConnectorStatus
+    chargingStation: ChargingStation,
+    transactionId: number | string
   ): OCPP20MeterValue[] {
-    return OCPP20ServiceUtils.buildEnergyMeterValues(
-      connectorStatus,
-      OCPP20ReadingContextEnumType.TRANSACTION_END
-    )
+    try {
+      const measurandsKey = buildConfigKey(
+        OCPP20ComponentName.SampledDataCtrlr,
+        StandardParametersKey.TxEndedMeasurands
+      )
+      const meterValue = buildMeterValue(
+        chargingStation,
+        transactionId,
+        0,
+        false,
+        measurandsKey,
+        OCPP20ReadingContextEnumType.TRANSACTION_END
+      ) as OCPP20MeterValue
+      return meterValue.sampledValue.length > 0 ? [meterValue] : []
+    } catch (error) {
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.buildTransactionEndedMeterValues: ${(error as Error).message}`
+      )
+      return []
+    }
   }
 
   private static readVariableAsBoolean (
@@ -851,7 +866,7 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils {
     stoppedReason: OCPP20ReasonEnumType,
     evseId?: number
   ): Promise<OCPP20TransactionEventResponse> {
-    const endedMeterValues = this.buildTransactionEndedMeterValues(connectorStatus)
+    const endedMeterValues = this.buildTransactionEndedMeterValues(chargingStation, transactionId)
 
     const response = await this.sendTransactionEvent(
       chargingStation,
index c47f84592a6ca03432222544b4d4a3f4b40b60b7..4f0f351863d344649fb8165c95958393672fefe0 100644 (file)
@@ -18,6 +18,7 @@ import {
   type AuthorizeRequest,
   ChargePointErrorCode,
   ChargingStationEvents,
+  type ConfigurationKeyType,
   type ConnectorStatus,
   ConnectorStatusEnum,
   CurrentType,
@@ -434,10 +435,10 @@ export const startTransactionOnConnector = async (
         }
         OCPP20ServiceUtils.resetTransactionSequenceNumber(chargingStation, connectorId)
       }
-      const startedMeterValues =
-        connectorStatus != null
-          ? OCPP20ServiceUtils.buildTransactionStartedMeterValues(connectorStatus)
-          : []
+      const startedMeterValues = OCPP20ServiceUtils.buildTransactionStartedMeterValues(
+        chargingStation,
+        transactionId
+      )
       const response = await OCPP20ServiceUtils.sendTransactionEvent(
         chargingStation,
         OCPP20TransactionEventEnumType.Started,
@@ -772,11 +773,13 @@ export const convertDateToISOString = <T extends JsonType>(object: T): void => {
 const buildSocMeasurandValue = (
   chargingStation: ChargingStation,
   connectorId: number,
-  evseId?: number
+  evseId?: number,
+  measurandsKey?: ConfigurationKeyType
 ): null | SingleValueMeasurandData => {
   const socSampledValueTemplate = getSampledValueTemplate(
     chargingStation,
     connectorId,
+    measurandsKey,
     MeterValueMeasurand.STATE_OF_CHARGE,
     evseId
   )
@@ -825,11 +828,13 @@ const validateSocMeasurandValue = (
 const buildVoltageMeasurandValue = (
   chargingStation: ChargingStation,
   connectorId: number,
-  evseId?: number
+  evseId?: number,
+  measurandsKey?: ConfigurationKeyType
 ): null | SingleValueMeasurandData => {
   const voltageSampledValueTemplate = getSampledValueTemplate(
     chargingStation,
     connectorId,
+    measurandsKey,
     MeterValueMeasurand.VOLTAGE,
     evseId
   )
@@ -862,7 +867,8 @@ const addMainVoltageToMeterValue = <TSampledValue extends SampledValue>(
     value: number,
     context?: MeterValueContext,
     phase?: MeterValuePhase
-  ) => TSampledValue
+  ) => TSampledValue,
+  context?: MeterValueContext
 ): void => {
   const stationInfo = chargingStation.stationInfo
   if (stationInfo == null) {
@@ -873,7 +879,7 @@ const addMainVoltageToMeterValue = <TSampledValue extends SampledValue>(
     (chargingStation.getNumberOfPhases() === 3 && stationInfo.mainVoltageMeterValues === true)
   ) {
     meterValue.sampledValue.push(
-      buildVersionedSampledValue(voltageData.template, voltageData.value)
+      buildVersionedSampledValue(voltageData.template, voltageData.value, context)
     )
   }
 }
@@ -889,7 +895,9 @@ const addPhaseVoltageToMeterValue = <TSampledValue extends SampledValue>(
     value: number,
     context?: MeterValueContext,
     phase?: MeterValuePhase
-  ) => TSampledValue
+  ) => TSampledValue,
+  measurandsKey?: ConfigurationKeyType,
+  context?: MeterValueContext
 ): void => {
   const stationInfo = chargingStation.stationInfo
   if (stationInfo == null) {
@@ -899,6 +907,7 @@ const addPhaseVoltageToMeterValue = <TSampledValue extends SampledValue>(
   const voltagePhaseLineToNeutralSampledValueTemplate = getSampledValueTemplate(
     chargingStation,
     connectorId,
+    measurandsKey,
     MeterValueMeasurand.VOLTAGE,
     undefined,
     phaseLineToNeutralValue
@@ -922,7 +931,7 @@ const addPhaseVoltageToMeterValue = <TSampledValue extends SampledValue>(
     buildVersionedSampledValue(
       voltagePhaseLineToNeutralSampledValueTemplate ?? mainVoltageData.template,
       voltagePhaseLineToNeutralMeasurandValue ?? mainVoltageData.value,
-      undefined,
+      context,
       phaseLineToNeutralValue
     )
   )
@@ -939,7 +948,9 @@ const addLineToLineVoltageToMeterValue = <TSampledValue extends SampledValue>(
     value: number,
     context?: MeterValueContext,
     phase?: MeterValuePhase
-  ) => TSampledValue
+  ) => TSampledValue,
+  measurandsKey?: ConfigurationKeyType,
+  context?: MeterValueContext
 ): void => {
   const stationInfo = chargingStation.stationInfo
   if (stationInfo?.phaseLineToLineVoltageMeterValues !== true) {
@@ -957,6 +968,7 @@ const addLineToLineVoltageToMeterValue = <TSampledValue extends SampledValue>(
   const voltagePhaseLineToLineSampledValueTemplate = getSampledValueTemplate(
     chargingStation,
     connectorId,
+    measurandsKey,
     MeterValueMeasurand.VOLTAGE,
     undefined,
     phaseLineToLineValue
@@ -980,7 +992,7 @@ const addLineToLineVoltageToMeterValue = <TSampledValue extends SampledValue>(
     buildVersionedSampledValue(
       voltagePhaseLineToLineSampledValueTemplate ?? mainVoltageData.template,
       voltagePhaseLineToLineMeasurandValue ?? voltagePhaseLineToLineValueRounded,
-      undefined,
+      context,
       phaseLineToLineValue
     )
   )
@@ -990,9 +1002,16 @@ const buildEnergyMeasurandValue = (
   chargingStation: ChargingStation,
   connectorId: number,
   interval: number,
-  evseId?: number
+  evseId?: number,
+  measurandsKey?: ConfigurationKeyType
 ): null | SingleValueMeasurandData => {
-  const energyTemplate = getSampledValueTemplate(chargingStation, connectorId, undefined, evseId)
+  const energyTemplate = getSampledValueTemplate(
+    chargingStation,
+    connectorId,
+    measurandsKey,
+    undefined,
+    evseId
+  )
   if (energyTemplate == null) {
     return null
   }
@@ -1073,11 +1092,13 @@ const validateEnergyMeasurandValue = (
 const buildPowerMeasurandValue = (
   chargingStation: ChargingStation,
   connectorId: number,
-  evseId?: number
+  evseId?: number,
+  measurandsKey?: ConfigurationKeyType
 ): MultiPhaseMeasurandData | null => {
   const powerTemplate = getSampledValueTemplate(
     chargingStation,
     connectorId,
+    measurandsKey,
     MeterValueMeasurand.POWER_ACTIVE_IMPORT,
     evseId
   )
@@ -1091,6 +1112,7 @@ const buildPowerMeasurandValue = (
       L1: getSampledValueTemplate(
         chargingStation,
         connectorId,
+        measurandsKey,
         MeterValueMeasurand.POWER_ACTIVE_IMPORT,
         evseId,
         MeterValuePhase.L1_N
@@ -1098,6 +1120,7 @@ const buildPowerMeasurandValue = (
       L2: getSampledValueTemplate(
         chargingStation,
         connectorId,
+        measurandsKey,
         MeterValueMeasurand.POWER_ACTIVE_IMPORT,
         evseId,
         MeterValuePhase.L2_N
@@ -1105,6 +1128,7 @@ const buildPowerMeasurandValue = (
       L3: getSampledValueTemplate(
         chargingStation,
         connectorId,
+        measurandsKey,
         MeterValueMeasurand.POWER_ACTIVE_IMPORT,
         evseId,
         MeterValuePhase.L3_N
@@ -1348,11 +1372,13 @@ const validateCurrentMeasurandPhaseValue = (
 const buildCurrentMeasurandValue = (
   chargingStation: ChargingStation,
   connectorId: number,
-  evseId?: number
+  evseId?: number,
+  measurandsKey?: ConfigurationKeyType
 ): MultiPhaseMeasurandData | null => {
   const currentTemplate = getSampledValueTemplate(
     chargingStation,
     connectorId,
+    measurandsKey,
     MeterValueMeasurand.CURRENT_IMPORT,
     evseId
   )
@@ -1366,6 +1392,7 @@ const buildCurrentMeasurandValue = (
       L1: getSampledValueTemplate(
         chargingStation,
         connectorId,
+        measurandsKey,
         MeterValueMeasurand.CURRENT_IMPORT,
         evseId,
         MeterValuePhase.L1
@@ -1373,6 +1400,7 @@ const buildCurrentMeasurandValue = (
       L2: getSampledValueTemplate(
         chargingStation,
         connectorId,
+        measurandsKey,
         MeterValueMeasurand.CURRENT_IMPORT,
         evseId,
         MeterValuePhase.L2
@@ -1380,6 +1408,7 @@ const buildCurrentMeasurandValue = (
       L3: getSampledValueTemplate(
         chargingStation,
         connectorId,
+        measurandsKey,
         MeterValueMeasurand.CURRENT_IMPORT,
         evseId,
         MeterValuePhase.L3
@@ -1548,7 +1577,9 @@ export const buildMeterValue = (
   chargingStation: ChargingStation,
   transactionId: number | string | undefined,
   interval: number,
-  debug = false
+  debug = false,
+  measurandsKey?: ConfigurationKeyType,
+  context?: MeterValueContext
 ): MeterValue => {
   if (transactionId == null) {
     return buildEmptyMeterValue()
@@ -1574,7 +1605,12 @@ export const buildMeterValue = (
         return buildSampledValueForOCPP16(sampledValueTemplate, value, context, phase)
       }
       // SoC measurand
-      const socMeasurand = buildSocMeasurandValue(chargingStation, connectorId)
+      const socMeasurand = buildSocMeasurandValue(
+        chargingStation,
+        connectorId,
+        undefined,
+        measurandsKey
+      )
       if (socMeasurand != null) {
         const socSampledValue = buildVersionedSampledValue(
           socMeasurand.template,
@@ -1591,7 +1627,12 @@ export const buildMeterValue = (
         )
       }
       // Voltage measurand
-      const voltageMeasurand = buildVoltageMeasurandValue(chargingStation, connectorId)
+      const voltageMeasurand = buildVoltageMeasurandValue(
+        chargingStation,
+        connectorId,
+        undefined,
+        measurandsKey
+      )
       if (voltageMeasurand != null) {
         addMainVoltageToMeterValue(
           chargingStation,
@@ -1623,7 +1664,12 @@ export const buildMeterValue = (
         }
       }
       // Power.Active.Import measurand
-      const powerMeasurand = buildPowerMeasurandValue(chargingStation, connectorId)
+      const powerMeasurand = buildPowerMeasurandValue(
+        chargingStation,
+        connectorId,
+        undefined,
+        measurandsKey
+      )
       if (powerMeasurand != null) {
         const unitDivider = powerMeasurand.template.unit === MeterValueUnit.KILO_WATT ? 1000 : 1
         const connectorMaximumAvailablePower =
@@ -1678,7 +1724,12 @@ export const buildMeterValue = (
         }
       }
       // Current.Import measurand
-      const currentMeasurand = buildCurrentMeasurandValue(chargingStation, connectorId)
+      const currentMeasurand = buildCurrentMeasurandValue(
+        chargingStation,
+        connectorId,
+        undefined,
+        measurandsKey
+      )
       if (currentMeasurand != null) {
         const connectorMaximumAvailablePower =
           chargingStation.getConnectorMaximumAvailablePower(connectorId)
@@ -1737,7 +1788,13 @@ export const buildMeterValue = (
         }
       }
       // Energy.Active.Import.Register measurand (default)
-      const energyMeasurand = buildEnergyMeasurandValue(chargingStation, connectorId, interval)
+      const energyMeasurand = buildEnergyMeasurandValue(
+        chargingStation,
+        connectorId,
+        interval,
+        undefined,
+        measurandsKey
+      )
       if (energyMeasurand != null) {
         updateConnectorEnergyValues(connectorStatus, energyMeasurand.value)
         const unitDivider =
@@ -1793,11 +1850,17 @@ export const buildMeterValue = (
         return buildSampledValueForOCPP20(sampledValueTemplate, value, context, phase)
       }
       // SoC measurand
-      const socMeasurand = buildSocMeasurandValue(chargingStation, connectorId, evseId)
+      const socMeasurand = buildSocMeasurandValue(
+        chargingStation,
+        connectorId,
+        evseId,
+        measurandsKey
+      )
       if (socMeasurand != null) {
         const socSampledValue = buildVersionedSampledValue(
           socMeasurand.template,
-          socMeasurand.value
+          socMeasurand.value,
+          context
         )
         meterValue.sampledValue.push(socSampledValue)
         validateSocMeasurandValue(
@@ -1810,13 +1873,19 @@ export const buildMeterValue = (
         )
       }
       // Voltage measurand
-      const voltageMeasurand = buildVoltageMeasurandValue(chargingStation, connectorId, evseId)
+      const voltageMeasurand = buildVoltageMeasurandValue(
+        chargingStation,
+        connectorId,
+        evseId,
+        measurandsKey
+      )
       if (voltageMeasurand != null) {
         addMainVoltageToMeterValue(
           chargingStation,
           meterValue,
           voltageMeasurand,
-          buildVersionedSampledValue
+          buildVersionedSampledValue,
+          context
         )
         for (
           let phase = 1;
@@ -1829,7 +1898,9 @@ export const buildMeterValue = (
             meterValue,
             voltageMeasurand,
             phase,
-            buildVersionedSampledValue
+            buildVersionedSampledValue,
+            measurandsKey,
+            context
           )
           addLineToLineVoltageToMeterValue(
             chargingStation,
@@ -1837,7 +1908,9 @@ export const buildMeterValue = (
             meterValue,
             voltageMeasurand,
             phase,
-            buildVersionedSampledValue
+            buildVersionedSampledValue,
+            measurandsKey,
+            context
           )
         }
       }
@@ -1846,7 +1919,8 @@ export const buildMeterValue = (
         chargingStation,
         connectorId,
         interval,
-        evseId
+        evseId,
+        measurandsKey
       )
       if (energyMeasurand != null) {
         updateConnectorEnergyValues(connectorStatus, energyMeasurand.value)
@@ -1858,7 +1932,8 @@ export const buildMeterValue = (
             chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
               unitDivider,
             2
-          )
+          ),
+          context
         )
         meterValue.sampledValue.push(energySampledValue)
         const connectorMaximumAvailablePower =
@@ -1880,20 +1955,32 @@ export const buildMeterValue = (
         )
       }
       // Power.Active.Import measurand
-      const powerMeasurand = buildPowerMeasurandValue(chargingStation, connectorId, evseId)
+      const powerMeasurand = buildPowerMeasurandValue(
+        chargingStation,
+        connectorId,
+        evseId,
+        measurandsKey
+      )
       if (powerMeasurand?.values.allPhases != null) {
         const powerSampledValue = buildVersionedSampledValue(
           powerMeasurand.template,
-          powerMeasurand.values.allPhases
+          powerMeasurand.values.allPhases,
+          context
         )
         meterValue.sampledValue.push(powerSampledValue)
       }
       // Current.Import measurand
-      const currentMeasurand = buildCurrentMeasurandValue(chargingStation, connectorId, evseId)
+      const currentMeasurand = buildCurrentMeasurandValue(
+        chargingStation,
+        connectorId,
+        evseId,
+        measurandsKey
+      )
       if (currentMeasurand?.values.allPhases != null) {
         const currentSampledValue = buildVersionedSampledValue(
           currentMeasurand.template,
-          currentMeasurand.values.allPhases
+          currentMeasurand.values.allPhases,
+          context
         )
         meterValue.sampledValue.push(currentSampledValue)
       }
@@ -2017,6 +2104,7 @@ const isMeasurandSupported = (measurand: MeterValueMeasurand): boolean => {
 const getSampledValueTemplate = (
   chargingStation: ChargingStation,
   connectorId: number,
+  measurandsKey: ConfigurationKeyType = StandardParametersKey.MeterValuesSampledData,
   measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
   evseId?: number,
   phase?: MeterValuePhase
@@ -2030,10 +2118,7 @@ const getSampledValueTemplate = (
   }
   if (
     measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
-    getConfigurationKey(
-      chargingStation,
-      StandardParametersKey.MeterValuesSampledData
-    )?.value?.includes(measurand) === false
+    getConfigurationKey(chargingStation, measurandsKey)?.value?.includes(measurand) === false
   ) {
     logger.debug(
       `${chargingStation.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId.toString()} not found in sampled data OCPP parameter`
@@ -2076,20 +2161,14 @@ const getSampledValueTemplate = (
       phase != null &&
       sampledValueTemplates[index].phase === phase &&
       sampledValueTemplates[index].measurand === measurand &&
-      getConfigurationKey(
-        chargingStation,
-        StandardParametersKey.MeterValuesSampledData
-      )?.value?.includes(measurand) === true
+      getConfigurationKey(chargingStation, measurandsKey)?.value?.includes(measurand) === true
     ) {
       return sampledValueTemplates[index]
     } else if (
       phase == null &&
       sampledValueTemplates[index].phase == null &&
       sampledValueTemplates[index].measurand === measurand &&
-      getConfigurationKey(
-        chargingStation,
-        StandardParametersKey.MeterValuesSampledData
-      )?.value?.includes(measurand) === true
+      getConfigurationKey(chargingStation, measurandsKey)?.value?.includes(measurand) === true
     ) {
       return sampledValueTemplates[index]
     } else if (
index 48db01f71efb6ee431587eb2ed8175fb39506467..6a33795ebd12f5c7b6a46081e7c2d64d24e278b0 100644 (file)
@@ -283,11 +283,9 @@ export const convertToBoolean = (value: unknown): boolean => {
     // Check the type
     if (typeof value === 'boolean') {
       return value
-    } else if (
-      typeof value === 'string' &&
-      (value.trim().toLowerCase() === 'true' || value === '1')
-    ) {
-      result = true
+    } else if (typeof value === 'string') {
+      const normalized = value.trim().toLowerCase()
+      result = normalized === 'true' || normalized === '1'
     } else if (typeof value === 'number' && value === 1) {
       result = true
     }
index 61cf02ca08792a9027a6df5b699b55f80a55d2dc..60eb3de08c482079ae9a1db9f25d4c13f5579e45 100644 (file)
@@ -24,8 +24,6 @@ import {
   OCPP20ChargingProfilePurposeEnumType,
   OCPP20IdTokenEnumType,
   OCPP20IncomingRequestCommand,
-  OCPP20MeasurandEnumType,
-  OCPP20ReadingContextEnumType,
   OCPP20RequestCommand,
   OCPP20TransactionEventEnumType,
   OCPP20TriggerReasonEnumType,
@@ -451,18 +449,6 @@ await describe('F01 & F02 - Remote Start Transaction', async () => {
       ]
       assert.strictEqual(args[1], OCPP20RequestCommand.TRANSACTION_EVENT)
       assert.strictEqual(args[2].eventType, OCPP20TransactionEventEnumType.Started)
-      assert.ok(
-        Array.isArray(args[2].meterValue) && args[2].meterValue.length > 0,
-        'TransactionEvent(Started) should include non-empty meterValue array'
-      )
-      assert.strictEqual(
-        args[2].meterValue[0].sampledValue[0].context,
-        OCPP20ReadingContextEnumType.TRANSACTION_BEGIN
-      )
-      assert.strictEqual(
-        args[2].meterValue[0].sampledValue[0].measurand,
-        OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER
-      )
     })
 
     await it('should NOT call TransactionEvent when response is Rejected', () => {
index f4f79bac1c7468c41d1a36e7b904bca69c03c9ae..eb015fa1f31522255838cc414a7911aa1cbb2786 100644 (file)
@@ -23,8 +23,6 @@ import { OCPPAuthServiceFactory } from '../../../../src/charging-station/ocpp/au
 import {
   OCPP20IdTokenEnumType,
   OCPP20IncomingRequestCommand,
-  OCPP20MeasurandEnumType,
-  OCPP20ReadingContextEnumType,
   OCPP20ReasonEnumType,
   OCPP20RequestCommand,
   OCPP20TransactionEventEnumType,
@@ -412,26 +410,6 @@ await describe('F03 - Remote Stop Transaction', async () => {
       const transactionEvent = args[2]
 
       assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Ended)
-
-      assert.notStrictEqual(transactionEvent.meterValue, undefined)
-      if (transactionEvent.meterValue == null) {
-        assert.fail('Expected meterValue to be defined')
-      }
-      assert.strictEqual(transactionEvent.meterValue.length, 1)
-
-      const meterValue = transactionEvent.meterValue[0]
-      assert.notStrictEqual(meterValue, undefined)
-      assert.ok(meterValue.timestamp instanceof Date)
-      assert.notStrictEqual(meterValue.sampledValue, undefined)
-      assert.strictEqual(meterValue.sampledValue.length, 1)
-
-      const sampledValue = meterValue.sampledValue[0]
-      assert.strictEqual(sampledValue.value, 12345.67)
-      assert.strictEqual(sampledValue.context, OCPP20ReadingContextEnumType.TRANSACTION_END)
-      assert.strictEqual(
-        sampledValue.measurand,
-        OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER
-      )
     })
   })
 })
index c213fe7a13ddff69f9add93ebfba18a5ad3c4144..dee306d6aeda4072724afdc65b8ce947037a7b23 100644 (file)
@@ -17,6 +17,7 @@ import type { ChargingStation } from '../../../../src/charging-station/index.js'
 import type { ConnectorStatus } from '../../../../src/types/ConnectorStatus.js'
 import type { EmptyObject } from '../../../../src/types/index.js'
 
+import { addConfigurationKey } from '../../../../src/charging-station/ConfigurationKeyUtils.js'
 import {
   buildTransactionEvent,
   OCPP20ServiceUtils,
@@ -31,9 +32,6 @@ import {
   OCPP20ComponentName,
   OCPP20IdTokenEnumType,
   type OCPP20IdTokenType,
-  OCPP20MeasurandEnumType,
-  OCPP20OperationalStatusEnumType,
-  OCPP20ReadingContextEnumType,
   OCPP20ReasonEnumType,
   OCPP20RequestCommand,
   OCPP20RequiredVariableName,
@@ -2457,22 +2455,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
       assert.strictEqual(txEvents.length, 2)
 
       const endedPayload = txEvents[1].payload
-      assert.notStrictEqual(endedPayload.meterValue, undefined)
-      const meterValues = endedPayload.meterValue as {
-        sampledValue: { context: string; measurand: string; value: number }[]
-        timestamp: Date
-      }[]
-      assert.strictEqual(meterValues.length, 1)
-      assert.strictEqual(meterValues[0].sampledValue.length, 1)
-      assert.strictEqual(
-        meterValues[0].sampledValue[0].measurand,
-        OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER
-      )
-      assert.strictEqual(meterValues[0].sampledValue[0].value, 1500)
-      assert.strictEqual(
-        meterValues[0].sampledValue[0].context,
-        OCPP20ReadingContextEnumType.TRANSACTION_END
-      )
+      assert.strictEqual(endedPayload.stoppedReason, OCPP20ReasonEnumType.DeAuthorized)
     })
 
     await it('should reset connector status after deauthorization', async () => {
@@ -2687,50 +2670,78 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
   })
 
   await describe('buildTransactionStartedMeterValues', async () => {
-    await it('should build meter value with Transaction.Begin context and energy register', () => {
-      const connectorStatus = {
-        availability: OCPP20OperationalStatusEnumType.Operative,
-        MeterValues: [],
-        transactionEnergyActiveImportRegisterValue: 1234,
-      } as unknown as ConnectorStatus
+    await it('should build meter values using TxStartedMeasurands config key', () => {
+      const { station } = createMockChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 3,
+        evseConfiguration: { evsesCount: 3 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        ocppRequestService: {
+          requestHandler: async () => Promise.resolve({} as EmptyObject),
+        },
+        stationInfo: {
+          ocppStrictCompliance: true,
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      })
+      resetLimits(station)
 
-      const result = OCPP20ServiceUtils.buildTransactionStartedMeterValues(connectorStatus)
+      // Set up energy MeterValues template on EVSE
+      const evseStatus = station.getEvseStatus(1)
+      if (evseStatus != null) {
+        evseStatus.MeterValues = [{ unit: 'Wh' }] as unknown as ConnectorStatus['MeterValues']
+      }
 
-      assert.strictEqual(result.length, 1)
-      assert.strictEqual(result[0].sampledValue.length, 1)
-      assert.strictEqual(
-        result[0].sampledValue[0].context,
-        OCPP20ReadingContextEnumType.TRANSACTION_BEGIN
-      )
-      assert.strictEqual(
-        result[0].sampledValue[0].measurand,
-        OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER
-      )
-      assert.strictEqual(result[0].sampledValue[0].value, 1234)
-      assert.ok(result[0].timestamp instanceof Date)
-    })
+      // Set up transaction
+      const transactionId = generateUUID()
+      const connectorStatus = station.getConnectorStatus(1)
+      if (connectorStatus != null) {
+        connectorStatus.transactionId = transactionId
+        connectorStatus.transactionStarted = true
+        connectorStatus.transactionEnergyActiveImportRegisterValue = 1234
+      }
 
-    await it('should include meter value with 0 energy when register value is undefined (zero is a valid begin reading)', () => {
-      const connectorStatus = {
-        availability: OCPP20OperationalStatusEnumType.Operative,
-        MeterValues: [],
-      } as unknown as ConnectorStatus
+      // Add TxStartedMeasurands config key with energy measurand
+      addConfigurationKey(
+        station,
+        `${OCPP20ComponentName.SampledDataCtrlr}.${OCPP20RequiredVariableName.TxStartedMeasurands}`,
+        'Energy.Active.Import.Register',
+        undefined,
+        { save: false }
+      )
 
-      const result = OCPP20ServiceUtils.buildTransactionStartedMeterValues(connectorStatus)
+      const result = OCPP20ServiceUtils.buildTransactionStartedMeterValues(station, transactionId)
 
       assert.strictEqual(result.length, 1)
-      assert.strictEqual(result[0].sampledValue[0].value, 0)
+      assert.ok(result[0].sampledValue.length >= 1)
+      assert.ok(result[0].timestamp instanceof Date)
     })
 
-    await it('should return empty array when energy register value is negative', () => {
-      const connectorStatus = {
-        availability: OCPP20OperationalStatusEnumType.Operative,
-        MeterValues: [],
-        transactionEnergyActiveImportRegisterValue: -1,
-      } as unknown as ConnectorStatus
+    await it('should return empty array when no transaction found for transactionId', () => {
+      const { station } = createMockChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 3,
+        evseConfiguration: { evsesCount: 3 },
+        heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+        ocppRequestService: {
+          requestHandler: async () => Promise.resolve({} as EmptyObject),
+        },
+        stationInfo: {
+          ocppStrictCompliance: true,
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+        websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+      })
+      resetLimits(station)
 
-      const result = OCPP20ServiceUtils.buildTransactionStartedMeterValues(connectorStatus)
+      // No transaction set up - transactionId won't resolve
+      const result = OCPP20ServiceUtils.buildTransactionStartedMeterValues(
+        station,
+        'non-existent-tx'
+      )
 
+      // buildMeterValue returns empty meter value when transactionId can't be resolved
       assert.strictEqual(result.length, 0)
     })
   })
index 8e1a72cdbbacfe60455af41a1ad687e30027623a..2bbb8127d70e7914571e8b454b88618ae50dccd5 100644 (file)
@@ -13,11 +13,9 @@ export const convertToBoolean = (value: unknown): boolean => {
     // Check the type
     if (typeof value === 'boolean') {
       return value
-    } else if (
-      typeof value === 'string' &&
-      (value.trim().toLowerCase() === 'true' || value === '1')
-    ) {
-      result = true
+    } else if (typeof value === 'string') {
+      const normalized = value.trim().toLowerCase()
+      result = normalized === 'true' || normalized === '1'
     } else if (typeof value === 'number' && value === 1) {
       result = true
     }