]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ocpp): enforce strict version separation in OCPPServiceUtils
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 30 Mar 2026 22:44:20 +0000 (00:44 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 30 Mar 2026 22:44:20 +0000 (00:44 +0200)
Extract version-specific logic from cross-version utilities:

- P1: Move buildStatusNotificationRequest into OCPP16ServiceUtils and
  OCPP20ServiceUtils as version-specific static methods
- P2: Move buildTransactionEndMeterValue to OCPP16ServiceUtils, remove
  dead OCPP 2.0 branch that was never called
- P3: Split buildMeterValue into thin dispatcher plus dedicated
  buildMeterValueForOCPP16/buildMeterValueForOCPP20 internal functions
- P4: Simplify checkConnectorStatusTransition to select transition
  tables by version then apply single shared lookup logic

Harmonize tests to match new module structure:
- Rename OCPPServiceUtils-StopTransaction.test.ts to
  OCPPServiceOperations.test.ts (functions moved to that module)
- Move mapStopReasonToOCPP20 tests to OCPPServiceUtils-pure.test.ts
  (function still lives in OCPPServiceUtils)
- Add dedicated unit tests for buildStatusNotificationRequest in both
  OCPP16ServiceUtils.test.ts and new OCPP20ServiceUtils-StatusNotification.test.ts
- Fix redundant describe nesting in OCPP20 test file

src/charging-station/ocpp/1.6/OCPP16RequestService.ts
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20RequestService.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
src/charging-station/ocpp/index.ts
tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-StatusNotification.test.ts [new file with mode: 0644]
tests/charging-station/ocpp/OCPPServiceOperations.test.ts [moved from tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts with 89% similarity]
tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts

index 63891195026850d993d24398e029adaebbd65770..255bbc7cf585538ef1e56d9a223b2af4b096e47b 100644 (file)
@@ -19,8 +19,6 @@ import {
 import { Constants, generateUUID, logger } from '../../../utils/index.js'
 import { OCPPRequestService } from '../OCPPRequestService.js'
 import {
-  buildStatusNotificationRequest,
-  buildTransactionEndMeterValue,
   createPayloadValidatorMap,
   isRequestCommandSupported,
   sendAndSetConnectorStatus,
@@ -215,8 +213,7 @@ export class OCPP16RequestService extends OCPPRequestService {
           ...commandParams,
         } as unknown as Request
       case OCPP16RequestCommand.STATUS_NOTIFICATION:
-        return buildStatusNotificationRequest(
-          chargingStation,
+        return OCPP16ServiceUtils.buildStatusNotificationRequest(
           commandParams as unknown as OCPP16StatusNotificationRequest
         ) as unknown as Request
       case OCPP16RequestCommand.STOP_TRANSACTION:
@@ -237,11 +234,11 @@ export class OCPP16RequestService extends OCPPRequestService {
             transactionData: OCPP16ServiceUtils.buildTransactionDataMeterValues(
               chargingStation.getConnectorStatus(connectorId)
                 ?.transactionBeginMeterValue as OCPP16MeterValue,
-              buildTransactionEndMeterValue(
+              OCPP16ServiceUtils.buildTransactionEndMeterValue(
                 chargingStation,
                 connectorId,
                 energyActiveImportRegister
-              ) as OCPP16MeterValue
+              )
             ),
           }),
           ...commandParams,
index 32cc33db27e199d19aff108c9fc9d11afa18eea0..c0f7b452ffc7ee4f3287915f5c5b29573ab27ed7 100644 (file)
@@ -19,7 +19,6 @@ import {
   type OCPP16AuthorizeResponse,
   type OCPP16BootNotificationResponse,
   OCPP16ChargePointStatus,
-  type OCPP16MeterValue,
   type OCPP16MeterValuesRequest,
   type OCPP16MeterValuesResponse,
   OCPP16RequestCommand,
@@ -38,7 +37,6 @@ import {
 import { Constants, convertToInt, logger, truncateId } from '../../../utils/index.js'
 import { OCPPResponseService } from '../OCPPResponseService.js'
 import {
-  buildTransactionEndMeterValue,
   createPayloadValidatorMap,
   isRequestCommandSupported,
   restoreConnectorStatus,
@@ -497,11 +495,11 @@ export class OCPP16ResponseService extends OCPPResponseService {
       >(chargingStation, OCPP16RequestCommand.METER_VALUES, {
         connectorId: transactionConnectorId,
         meterValue: [
-          buildTransactionEndMeterValue(
+          OCPP16ServiceUtils.buildTransactionEndMeterValue(
             chargingStation,
             transactionConnectorId,
             requestPayload.meterStop
-          ) as OCPP16MeterValue,
+          ),
         ],
         transactionId: requestPayload.transactionId,
       }))
index 026ba05e949463ba92f20f0255f827e64d380e0b..7f6aa814746cbdcef3566abe760aead80b737b6b 100644 (file)
@@ -13,7 +13,9 @@ import {
   hasFeatureProfile,
   hasReservationExpired,
 } from '../../../charging-station/index.js'
+import { BaseError } from '../../../exception/index.js'
 import {
+  ChargePointErrorCode,
   type ConfigurationKey,
   type GenericResponse,
   type MeterValuesRequest,
@@ -28,6 +30,7 @@ import {
   OCPP16IncomingRequestCommand,
   type OCPP16MeterValue,
   OCPP16MeterValueContext,
+  OCPP16MeterValueMeasurand,
   OCPP16MeterValueUnit,
   OCPP16RequestCommand,
   type OCPP16SampledValue,
@@ -55,7 +58,6 @@ import {
   buildEmptyMeterValue,
   buildMeterValue,
   buildSampledValue,
-  buildTransactionEndMeterValue,
   getSampledValueTemplate,
   PayloadValidatorConfig,
   PayloadValidatorOptions,
@@ -103,6 +105,20 @@ export class OCPP16ServiceUtils {
     [OCPP16RequestCommand.STOP_TRANSACTION, 'StopTransaction'],
   ]
 
+  /**
+   * @param commandParams - Status notification parameters
+   * @returns Formatted OCPP 1.6 StatusNotification request payload
+   */
+  public static buildStatusNotificationRequest (
+    commandParams: OCPP16StatusNotificationRequest
+  ): OCPP16StatusNotificationRequest {
+    return {
+      connectorId: commandParams.connectorId,
+      errorCode: ChargePointErrorCode.NO_ERROR,
+      status: commandParams.status,
+    } satisfies OCPP16StatusNotificationRequest
+  }
+
   /**
    * Builds a meter value for the beginning of a transaction.
    * @param chargingStation - Target charging station
@@ -149,6 +165,36 @@ export class OCPP16ServiceUtils {
     return meterValues
   }
 
+  /**
+   * @param chargingStation - Target charging station
+   * @param connectorId - Connector ID associated with the transaction
+   * @param meterStop - Final meter reading in Wh at transaction end
+   * @returns MeterValue containing the transaction end energy reading
+   */
+  public static buildTransactionEndMeterValue (
+    chargingStation: ChargingStation,
+    connectorId: number,
+    meterStop: number | undefined
+  ): OCPP16MeterValue {
+    const sampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId)
+    if (sampledValueTemplate == null) {
+      throw new BaseError(
+        `Missing MeterValues for default measurand '${OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}' in template on connector id ${connectorId.toString()}`
+      )
+    }
+    const unitDivider = sampledValueTemplate.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
+    const meterValue = buildEmptyMeterValue() as OCPP16MeterValue
+    meterValue.sampledValue.push(
+      buildSampledValue(
+        OCPPVersion.VERSION_16,
+        sampledValueTemplate,
+        roundTo((meterStop ?? 0) / unitDivider, 4),
+        OCPP16MeterValueContext.TRANSACTION_END
+      ) as OCPP16SampledValue
+    )
+    return meterValue
+  }
+
   /**
    * Changes the availability of connectors and updates their status.
    * @param chargingStation - Target charging station
@@ -749,7 +795,7 @@ export class OCPP16ServiceUtils {
       chargingStation.stationInfo.ocppStrictCompliance === true &&
       chargingStation.stationInfo.outOfOrderEndMeterValues === false
     ) {
-      const transactionEndMeterValue = buildTransactionEndMeterValue(
+      const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
         chargingStation,
         connectorId,
         chargingStation.getEnergyActiveImportRegisterByTransactionId(rawTransactionId)
index f5435a7b0dfe6adb5d771929ca7303b1e9666c20..0b1a4e1539b46cee2a26c915dc9d719e454f465b 100644 (file)
@@ -18,11 +18,7 @@ import {
 } from '../../../types/index.js'
 import { generateUUID, logger } from '../../../utils/index.js'
 import { OCPPRequestService } from '../OCPPRequestService.js'
-import {
-  buildStatusNotificationRequest,
-  createPayloadValidatorMap,
-  isRequestCommandSupported,
-} from '../OCPPServiceUtils.js'
+import { createPayloadValidatorMap, isRequestCommandSupported } from '../OCPPServiceUtils.js'
 import { generatePkcs10Csr } from './Asn1DerUtils.js'
 import { OCPP20Constants } from './OCPP20Constants.js'
 import { buildTransactionEvent, OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
@@ -202,7 +198,7 @@ export class OCPP20RequestService extends OCPPRequestService {
         return requestPayload as unknown as Request
       }
       case OCPP20RequestCommand.STATUS_NOTIFICATION:
-        return buildStatusNotificationRequest(
+        return OCPP20ServiceUtils.buildStatusNotificationRequest(
           chargingStation,
           commandParams as unknown as OCPP20StatusNotificationRequest
         ) as unknown as Request
index 4f875bd690e05a2e50d48766db8db5c709cd0c1f..64e304dbc901d698776ef8fab937583f59d65f00 100644 (file)
@@ -8,6 +8,7 @@ import {
   ErrorType,
   OCPP20ChargingStateEnumType,
   OCPP20ComponentName,
+  type OCPP20ConnectorStatusEnumType,
   type OCPP20EVSEType,
   OCPP20IncomingRequestCommand,
   type OCPP20MeterValue,
@@ -25,6 +26,7 @@ import {
   OCPP20TriggerReasonEnumType,
   OCPPVersion,
   ReasonCodeEnumType,
+  RequestCommand,
   type UUIDv4,
 } from '../../../types/index.js'
 import {
@@ -100,6 +102,35 @@ export class OCPP20ServiceUtils {
     [OCPP20RequestCommand.TRANSACTION_EVENT, 'TransactionEvent'],
   ]
 
+  /**
+   * @param chargingStation - Target charging station for EVSE resolution
+   * @param commandParams - Status notification parameters
+   * @returns Formatted OCPP 2.0 StatusNotification request payload
+   */
+  public static buildStatusNotificationRequest (
+    chargingStation: ChargingStation,
+    commandParams: OCPP20StatusNotificationRequest
+  ): OCPP20StatusNotificationRequest {
+    const params = commandParams as Record<string, unknown>
+    const connectorId = params.connectorId as number
+    const connectorStatus = (params.connectorStatus ?? params.status) as ConnectorStatusEnum
+    const evseId = params.evseId as number | undefined
+    const resolvedEvseId = evseId ?? chargingStation.getEvseIdByConnectorId(connectorId)
+    if (resolvedEvseId === undefined) {
+      throw new OCPPError(
+        ErrorType.INTERNAL_ERROR,
+        `Cannot build status notification payload: evseId is undefined for connector ${connectorId.toString()}`,
+        RequestCommand.STATUS_NOTIFICATION
+      )
+    }
+    return {
+      connectorId,
+      connectorStatus: connectorStatus as OCPP20ConnectorStatusEnumType,
+      evseId: resolvedEvseId,
+      timestamp: new Date(),
+    } satisfies OCPP20StatusNotificationRequest
+  }
+
   /**
    * Build meter values for the start of a transaction.
    * @param chargingStation - Target charging station
index 37ac28980b712d1c370c0c316affb9bb398b13a3..81f42a0103d8b6c08fc987d91872b05892dcb9eb 100644 (file)
@@ -10,7 +10,6 @@ import type { StopTransactionReason } from '../../types/index.js'
 import { type ChargingStation, getConfigurationKey } from '../../charging-station/index.js'
 import { BaseError, OCPPError } from '../../exception/index.js'
 import {
-  ChargePointErrorCode,
   ChargingStationEvents,
   type ConfigurationKeyType,
   type ConnectorStatus,
@@ -31,13 +30,10 @@ import {
   MeterValueUnit,
   type OCPP16MeterValue,
   type OCPP16SampledValue,
-  type OCPP16StatusNotificationRequest,
   OCPP16StopTransactionReason,
-  type OCPP20ConnectorStatusEnumType,
   type OCPP20MeterValue,
   OCPP20ReasonEnumType,
   type OCPP20SampledValue,
-  type OCPP20StatusNotificationRequest,
   OCPP20TriggerReasonEnumType,
   OCPPVersion,
   RequestCommand,
@@ -85,56 +81,6 @@ interface SingleValueMeasurandData {
   value: number
 }
 
-/**
- * Builds a StatusNotification request payload for the appropriate OCPP version.
- * @param chargingStation - Target charging station
- * @param commandParams - Status notification parameters including connector ID and status
- * @returns Formatted StatusNotification request payload
- */
-export const buildStatusNotificationRequest = (
-  chargingStation: ChargingStation,
-  commandParams: StatusNotificationRequest
-): StatusNotificationRequest => {
-  switch (chargingStation.stationInfo?.ocppVersion) {
-    case OCPPVersion.VERSION_16: {
-      const params = commandParams as OCPP16StatusNotificationRequest
-      return {
-        connectorId: params.connectorId,
-        errorCode: ChargePointErrorCode.NO_ERROR,
-        status: params.status,
-      } satisfies OCPP16StatusNotificationRequest
-    }
-    case OCPPVersion.VERSION_20:
-    case OCPPVersion.VERSION_201: {
-      const params = commandParams as Record<string, unknown>
-      const connectorId = params.connectorId as number
-      const connectorStatus = (params.connectorStatus ?? params.status) as ConnectorStatusEnum
-      const evseId = params.evseId as number | undefined
-      const resolvedEvseId = evseId ?? chargingStation.getEvseIdByConnectorId(connectorId)
-      if (resolvedEvseId === undefined) {
-        throw new OCPPError(
-          ErrorType.INTERNAL_ERROR,
-          `Cannot build status notification payload: evseId is undefined for connector ${connectorId.toString()}`,
-          RequestCommand.STATUS_NOTIFICATION
-        )
-      }
-      return {
-        connectorId,
-        connectorStatus: connectorStatus as OCPP20ConnectorStatusEnumType,
-        evseId: resolvedEvseId,
-        timestamp: new Date(),
-      } satisfies OCPP20StatusNotificationRequest
-    }
-    default:
-      throw new OCPPError(
-        ErrorType.INTERNAL_ERROR,
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        `Cannot build status notification payload: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
-        RequestCommand.STATUS_NOTIFICATION
-      )
-  }
-}
-
 /**
  * Sends a StatusNotification request and updates the connector status locally.
  * @param chargingStation - Target charging station
@@ -275,39 +221,18 @@ const checkConnectorStatusTransition = (
   status: ConnectorStatusEnum
 ): boolean => {
   const fromStatus = chargingStation.getConnectorStatus(connectorId)?.status
-  let transitionAllowed = false
+  let chargingStationTransitions: readonly { from?: ConnectorStatusEnum; to: ConnectorStatusEnum }[]
+  let connectorTransitions: readonly { from?: ConnectorStatusEnum; to: ConnectorStatusEnum }[]
   switch (chargingStation.stationInfo?.ocppVersion) {
-    case OCPPVersion.VERSION_16: {
-      if (
-        (connectorId === 0 &&
-          OCPP16Constants.ChargePointStatusChargingStationTransitions.findIndex(
-            transition => transition.from === fromStatus && transition.to === status
-          ) !== -1) ||
-        (connectorId > 0 &&
-          OCPP16Constants.ChargePointStatusConnectorTransitions.findIndex(
-            transition => transition.from === fromStatus && transition.to === status
-          ) !== -1)
-      ) {
-        transitionAllowed = true
-      }
+    case OCPPVersion.VERSION_16:
+      chargingStationTransitions = OCPP16Constants.ChargePointStatusChargingStationTransitions
+      connectorTransitions = OCPP16Constants.ChargePointStatusConnectorTransitions
       break
-    }
     case OCPPVersion.VERSION_20:
-    case OCPPVersion.VERSION_201: {
-      if (
-        (connectorId === 0 &&
-          OCPP20Constants.ChargingStationStatusTransitions.findIndex(
-            transition => transition.from === fromStatus && transition.to === status
-          ) !== -1) ||
-        (connectorId > 0 &&
-          OCPP20Constants.ConnectorStatusTransitions.findIndex(
-            transition => transition.from === fromStatus && transition.to === status
-          ) !== -1)
-      ) {
-        transitionAllowed = true
-      }
+    case OCPPVersion.VERSION_201:
+      chargingStationTransitions = OCPP20Constants.ChargingStationStatusTransitions
+      connectorTransitions = OCPP20Constants.ConnectorStatusTransitions
       break
-    }
     default:
       throw new OCPPError(
         ErrorType.INTERNAL_ERROR,
@@ -316,6 +241,10 @@ const checkConnectorStatusTransition = (
         RequestCommand.STATUS_NOTIFICATION
       )
   }
+  const transitions = connectorId === 0 ? chargingStationTransitions : connectorTransitions
+  const transitionAllowed = transitions.some(
+    transition => transition.from === fromStatus && transition.to === status
+  )
   if (!transitionAllowed) {
     logger.warn(
       `${chargingStation.logPrefix()} OCPP ${
@@ -1214,468 +1143,434 @@ export const buildMeterValue = (
     return buildEmptyMeterValue()
   }
   switch (chargingStation.stationInfo?.ocppVersion) {
-    case OCPPVersion.VERSION_16: {
-      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 buildSampledValueForOCPP16(sampledValueTemplate, value, context, phase)
-      }
-      // SoC measurand
-      const socMeasurand = buildSocMeasurandValue(
+    case OCPPVersion.VERSION_16:
+      return buildMeterValueForOCPP16(
         chargingStation,
-        connectorId,
-        undefined,
-        measurandsKey
+        transactionId,
+        interval,
+        measurandsKey,
+        context,
+        debug
       )
-      if (socMeasurand != null) {
-        const socSampledValue = buildVersionedSampledValue(
-          socMeasurand.template,
-          socMeasurand.value
-        )
-        meterValue.sampledValue.push(socSampledValue)
-        validateSocMeasurandValue(
-          chargingStation,
-          connectorId,
-          socSampledValue,
-          socMeasurand.template.minimumValue ?? 0,
-          100,
-          debug
-        )
-      }
-      // Voltage measurand
-      const voltageMeasurand = buildVoltageMeasurandValue(
+    case OCPPVersion.VERSION_20:
+    case OCPPVersion.VERSION_201:
+      return buildMeterValueForOCPP20(
         chargingStation,
-        connectorId,
-        undefined,
-        measurandsKey
+        transactionId,
+        interval,
+        measurandsKey,
+        context,
+        debug
       )
-      if (voltageMeasurand != null) {
-        addMainVoltageToMeterValue(
-          chargingStation,
-          meterValue,
-          voltageMeasurand,
-          buildVersionedSampledValue
-        )
-        for (
-          let phase = 1;
-          chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
-          phase++
-        ) {
-          addPhaseVoltageToMeterValue(
-            chargingStation,
-            connectorId,
-            meterValue,
-            voltageMeasurand,
-            phase,
-            buildVersionedSampledValue
-          )
-          addLineToLineVoltageToMeterValue(
-            chargingStation,
-            connectorId,
-            meterValue,
-            voltageMeasurand,
-            phase,
-            buildVersionedSampledValue
-          )
-        }
-      }
-      // Power.Active.Import measurand
-      const powerMeasurand = buildPowerMeasurandValue(
+    default:
+      throw new OCPPError(
+        ErrorType.INTERNAL_ERROR,
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
+        RequestCommand.METER_VALUES
+      )
+  }
+}
+
+const buildMeterValueForOCPP16 = (
+  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 buildSampledValueForOCPP16(sampledValueTemplate, value, context, phase)
+  }
+  // SoC measurand
+  const socMeasurand = buildSocMeasurandValue(
+    chargingStation,
+    connectorId,
+    undefined,
+    measurandsKey
+  )
+  if (socMeasurand != null) {
+    const socSampledValue = buildVersionedSampledValue(socMeasurand.template, socMeasurand.value)
+    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
+    )
+    for (
+      let phase = 1;
+      chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
+      phase++
+    ) {
+      addPhaseVoltageToMeterValue(
         chargingStation,
         connectorId,
-        undefined,
-        measurandsKey
+        meterValue,
+        voltageMeasurand,
+        phase,
+        buildVersionedSampledValue
       )
-      if (powerMeasurand != 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)
-        )
-        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, undefined, 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(
+      addLineToLineVoltageToMeterValue(
         chargingStation,
         connectorId,
-        undefined,
-        measurandsKey
+        meterValue,
+        voltageMeasurand,
+        phase,
+        buildVersionedSampledValue
       )
-      if (currentMeasurand != 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)
-        )
-        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
+    }
+  }
+  // Power.Active.Import measurand
+  const powerMeasurand = buildPowerMeasurandValue(
+    chargingStation,
+    connectorId,
+    undefined,
+    measurandsKey
+  )
+  if (powerMeasurand != 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)
+    )
+    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(
-              currentMeasurand.perPhaseTemplates[
-                phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
-              ] ?? currentMeasurand.template,
-              currentMeasurand.values[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
-              undefined,
-              phaseValue
-            )
+            buildVersionedSampledValue(phaseTemplate, phasePowerValue, undefined, phaseValue)
           )
           const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
-          validateCurrentMeasurandPhaseValue(
+          validatePowerMeasurandValue(
             chargingStation,
             connectorId,
             connectorStatus,
             meterValue.sampledValue[sampledValuesPerPhaseIndex],
-            connectorMaximumAmperage,
-            connectorMinimumAmperage,
+            connectorMaximumPowerPerPhase / unitDivider,
+            connectorMinimumPowerPerPhase / unitDivider,
             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
-          )
-        )
-        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
     }
-    case OCPPVersion.VERSION_20:
-    case OCPPVersion.VERSION_201: {
-      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 buildSampledValueForOCPP20(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
-          )
-        }
-      }
-      // Energy.Active.Import.Register measurand
-      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
+  }
+  // Current.Import measurand
+  const currentMeasurand = buildCurrentMeasurandValue(
+    chargingStation,
+    connectorId,
+    undefined,
+    measurandsKey
+  )
+  if (currentMeasurand != null) {
+    const connectorMaximumAvailablePower =
+      chargingStation.getConnectorMaximumAvailablePower(connectorId)
+    const connectorMaximumAmperage =
+      chargingStation.stationInfo?.currentOutType === CurrentType.AC
+        ? ACElectricUtils.amperagePerPhaseFromPower(
+          chargingStation.getNumberOfPhases(),
+          connectorMaximumAvailablePower,
+          chargingStation.getVoltageOut()
         )
-        const connectorMinimumEnergyRounded = roundTo(energyMeasurand.template.minimumValue ?? 0, 2)
-        validateEnergyMeasurandValue(
-          chargingStation,
-          connectorId,
-          energySampledValue,
-          energyMeasurand.value,
-          connectorMinimumEnergyRounded,
-          connectorMaximumEnergyRounded,
-          interval,
-          debug
+        : DCElectricUtils.amperage(connectorMaximumAvailablePower, chargingStation.getVoltageOut())
+    const connectorMinimumAmperage = currentMeasurand.template.minimumValue ?? 0
+
+    meterValue.sampledValue.push(
+      buildVersionedSampledValue(currentMeasurand.template, currentMeasurand.values.allPhases)
+    )
+    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],
+          undefined,
+          phaseValue
         )
-      }
-      // Power.Active.Import measurand
-      const powerMeasurand = buildPowerMeasurandValue(
-        chargingStation,
-        connectorId,
-        evseId,
-        measurandsKey
       )
-      if (powerMeasurand?.values.allPhases != null) {
-        const powerSampledValue = buildVersionedSampledValue(
-          powerMeasurand.template,
-          powerMeasurand.values.allPhases,
-          context
-        )
-        meterValue.sampledValue.push(powerSampledValue)
-      }
-      // Current.Import measurand
-      const currentMeasurand = buildCurrentMeasurandValue(
+      const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
+      validateCurrentMeasurandPhaseValue(
         chargingStation,
         connectorId,
-        evseId,
-        measurandsKey
+        connectorStatus,
+        meterValue.sampledValue[sampledValuesPerPhaseIndex],
+        connectorMaximumAmperage,
+        connectorMinimumAmperage,
+        debug
       )
-      if (currentMeasurand?.values.allPhases != null) {
-        const currentSampledValue = buildVersionedSampledValue(
-          currentMeasurand.template,
-          currentMeasurand.values.allPhases,
-          context
-        )
-        meterValue.sampledValue.push(currentSampledValue)
-      }
-      return meterValue
     }
-    default:
-      throw new OCPPError(
-        ErrorType.INTERNAL_ERROR,
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
-        RequestCommand.METER_VALUES
+  }
+  // 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
       )
+    )
+    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 a MeterValue for the end of a transaction with the final energy register value.
- * @param chargingStation - Target charging station
- * @param connectorId - Connector ID associated with the transaction
- * @param meterStop - Final meter reading in Wh at transaction end
- * @returns MeterValue containing the transaction end energy reading
- */
-export const buildTransactionEndMeterValue = (
+const buildMeterValueForOCPP20 = (
   chargingStation: ChargingStation,
-  connectorId: number,
-  meterStop: number | undefined
-): MeterValue => {
-  const sampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId)
-  if (sampledValueTemplate == null) {
-    throw new BaseError(
-      `Missing MeterValues for default measurand '${MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}' in template on connector id ${connectorId.toString()}`
+  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 unitDivider = sampledValueTemplate.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
-  switch (chargingStation.stationInfo?.ocppVersion) {
-    case OCPPVersion.VERSION_16: {
-      const meterValue = buildEmptyMeterValue() as OCPP16MeterValue
-      meterValue.sampledValue.push(
-        buildSampledValueForOCPP16(
-          sampledValueTemplate,
-          roundTo((meterStop ?? 0) / unitDivider, 4),
-          MeterValueContext.TRANSACTION_END
-        )
+  const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+  const meterValue = buildEmptyMeterValue() as OCPP20MeterValue
+  const buildVersionedSampledValue = (
+    sampledValueTemplate: SampledValueTemplate,
+    value: number,
+    context?: MeterValueContext,
+    phase?: MeterValuePhase
+  ): OCPP20SampledValue => {
+    return buildSampledValueForOCPP20(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
       )
-      return meterValue
-    }
-    case OCPPVersion.VERSION_20:
-    case OCPPVersion.VERSION_201: {
-      const meterValue = buildEmptyMeterValue() as OCPP20MeterValue
-      meterValue.sampledValue.push(
-        buildSampledValueForOCPP20(
-          sampledValueTemplate,
-          roundTo((meterStop ?? 0) / unitDivider, 4),
-          MeterValueContext.TRANSACTION_END
-        )
+      addLineToLineVoltageToMeterValue(
+        chargingStation,
+        connectorId,
+        meterValue,
+        voltageMeasurand,
+        phase,
+        buildVersionedSampledValue,
+        measurandsKey,
+        context
       )
-      return meterValue
     }
-    default:
-      throw new OCPPError(
-        ErrorType.INTERNAL_ERROR,
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
-        RequestCommand.METER_VALUES
-      )
   }
+  // Energy.Active.Import.Register measurand
+  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
+    )
+  }
+  // Power.Active.Import measurand
+  const powerMeasurand = buildPowerMeasurandValue(
+    chargingStation,
+    connectorId,
+    evseId,
+    measurandsKey
+  )
+  if (powerMeasurand?.values.allPhases != null) {
+    const powerSampledValue = buildVersionedSampledValue(
+      powerMeasurand.template,
+      powerMeasurand.values.allPhases,
+      context
+    )
+    meterValue.sampledValue.push(powerSampledValue)
+  }
+  // Current.Import measurand
+  const currentMeasurand = buildCurrentMeasurandValue(
+    chargingStation,
+    connectorId,
+    evseId,
+    measurandsKey
+  )
+  if (currentMeasurand?.values.allPhases != null) {
+    const currentSampledValue = buildVersionedSampledValue(
+      currentMeasurand.template,
+      currentMeasurand.values.allPhases,
+      context
+    )
+    meterValue.sampledValue.push(currentSampledValue)
+  }
+  return meterValue
 }
 
 const checkMeasurandPowerDivider = (
index ed86ff6329b01a688ea8ed6646fd8816c291a8b7..60d32f077b333492d88ab8e6afbbefb4c48cba92 100644 (file)
@@ -17,9 +17,4 @@ export {
   stopRunningTransactions,
   stopTransactionOnConnector,
 } from './OCPPServiceOperations.js'
-export {
-  buildMeterValue,
-  buildStatusNotificationRequest,
-  buildTransactionEndMeterValue,
-  sendAndSetConnectorStatus,
-} from './OCPPServiceUtils.js'
+export { buildMeterValue, sendAndSetConnectorStatus } from './OCPPServiceUtils.js'
index 40cb28d4a89cc4e70a1e2b6e49951a873fec60cb..720a7c9ec32509a0c81696379a1cb172d1050298 100644 (file)
@@ -11,11 +11,12 @@ import { afterEach, describe, it } from 'node:test'
 
 import { OCPP16ServiceUtils } from '../../../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js'
 import {
-  buildTransactionEndMeterValue,
   isIncomingRequestCommandSupported,
   isRequestCommandSupported,
 } from '../../../../src/charging-station/ocpp/OCPPServiceUtils.js'
 import {
+  ChargePointErrorCode,
+  OCPP16ChargePointStatus,
   type OCPP16ChargingProfile,
   OCPP16ChargingProfileKindType,
   OCPP16ChargingProfilePurposeType,
@@ -29,6 +30,7 @@ import {
   OCPP16MeterValueUnit,
   OCPP16RequestCommand,
   OCPP16StandardParametersKey,
+  type OCPP16StatusNotificationRequest,
   OCPP16SupportedFeatureProfiles,
   OCPPVersion,
 } from '../../../../src/types/index.js'
@@ -233,7 +235,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => {
       }
 
       // Act
-      const meterValue = buildTransactionEndMeterValue(station, 1, 10000)
+      const meterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(station, 1, 10000)
 
       // Assert
       assert.notStrictEqual(meterValue, undefined)
@@ -263,7 +265,7 @@ await describe('OCPP16ServiceUtils — pure functions', async () => {
       }
 
       // Act
-      const meterValue = buildTransactionEndMeterValue(station, 1, 3000)
+      const meterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(station, 1, 3000)
 
       // Assert — kWh divider: 3000 / 1000 = 3
       assert.strictEqual(meterValue.sampledValue[0].value, '3')
@@ -667,6 +669,58 @@ await describe('OCPP16ServiceUtils — pure functions', async () => {
     })
   })
 
+  // ─── buildStatusNotificationRequest ─────────────────────────────────────
+
+  await describe('buildStatusNotificationRequest', async () => {
+    await it('should return payload with NO_ERROR error code', () => {
+      const input: OCPP16StatusNotificationRequest = {
+        connectorId: 1,
+        errorCode: ChargePointErrorCode.NO_ERROR,
+        status: OCPP16ChargePointStatus.Available,
+      }
+
+      const result = OCPP16ServiceUtils.buildStatusNotificationRequest(input)
+
+      assert.strictEqual(result.errorCode, ChargePointErrorCode.NO_ERROR)
+    })
+
+    await it('should preserve connectorId from input', () => {
+      const input: OCPP16StatusNotificationRequest = {
+        connectorId: 2,
+        errorCode: ChargePointErrorCode.NO_ERROR,
+        status: OCPP16ChargePointStatus.Charging,
+      }
+
+      const result = OCPP16ServiceUtils.buildStatusNotificationRequest(input)
+
+      assert.strictEqual(result.connectorId, 2)
+    })
+
+    await it('should preserve status from input', () => {
+      const input: OCPP16StatusNotificationRequest = {
+        connectorId: 1,
+        errorCode: ChargePointErrorCode.NO_ERROR,
+        status: OCPP16ChargePointStatus.Charging,
+      }
+
+      const result = OCPP16ServiceUtils.buildStatusNotificationRequest(input)
+
+      assert.strictEqual(result.status, OCPP16ChargePointStatus.Charging)
+    })
+
+    await it('should always set errorCode to NO_ERROR regardless of input errorCode', () => {
+      const input: OCPP16StatusNotificationRequest = {
+        connectorId: 1,
+        errorCode: ChargePointErrorCode.CONNECTOR_LOCK_FAILURE,
+        status: OCPP16ChargePointStatus.Faulted,
+      }
+
+      const result = OCPP16ServiceUtils.buildStatusNotificationRequest(input)
+
+      assert.strictEqual(result.errorCode, ChargePointErrorCode.NO_ERROR)
+    })
+  })
+
   // ─── isConfigurationKeyVisible ─────────────────────────────────────────
 
   await describe('isConfigurationKeyVisible', async () => {
diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-StatusNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-StatusNotification.test.ts
new file mode 100644 (file)
index 0000000..4dd4760
--- /dev/null
@@ -0,0 +1,146 @@
+/**
+ * @file Tests for OCPP20ServiceUtils buildStatusNotificationRequest
+ * @description Unit tests for OCPP 2.0 StatusNotification request building,
+ * including EVSE resolution, connector status mapping, and error handling.
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
+import { OCPPError } from '../../../../src/exception/index.js'
+import {
+  OCPP20ConnectorStatusEnumType,
+  type OCPP20StatusNotificationRequest,
+  OCPPVersion,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('OCPP20ServiceUtils', async () => {
+  let mockStation: ChargingStation
+
+  beforeEach(() => {
+    const { station } = createMockChargingStation({
+      baseName: TEST_CHARGING_STATION_BASE_NAME,
+      connectorsCount: 2,
+      evseConfiguration: { evsesCount: 2 },
+      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+      stationInfo: {
+        ocppStrictCompliance: true,
+        ocppVersion: OCPPVersion.VERSION_201,
+      },
+      websocketPingInterval: Constants.DEFAULT_WS_PING_INTERVAL,
+    })
+    mockStation = station
+  })
+
+  afterEach(() => {
+    standardCleanup()
+  })
+
+  await describe('buildStatusNotificationRequest', async () => {
+    await it('should resolve evseId from chargingStation when not provided', () => {
+      const input = {
+        connectorId: 1,
+        connectorStatus: OCPP20ConnectorStatusEnumType.Available,
+      } as OCPP20StatusNotificationRequest
+
+      const result = OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input)
+
+      assert.strictEqual(typeof result.evseId, 'number')
+      assert.notStrictEqual(result.evseId, undefined)
+    })
+
+    await it('should use provided evseId when present', () => {
+      const input = {
+        connectorId: 1,
+        connectorStatus: OCPP20ConnectorStatusEnumType.Available,
+        evseId: 42,
+      } as OCPP20StatusNotificationRequest
+
+      const result = OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input)
+
+      assert.strictEqual(result.evseId, 42)
+    })
+
+    await it('should throw OCPPError when evseId cannot be resolved', () => {
+      // Arrange
+      const { station: noEvseStation } = createMockChargingStation({
+        baseName: TEST_CHARGING_STATION_BASE_NAME,
+        connectorsCount: 0,
+        stationInfo: {
+          ocppStrictCompliance: true,
+          ocppVersion: OCPPVersion.VERSION_201,
+        },
+      })
+      const input = {
+        connectorId: 999,
+        connectorStatus: OCPP20ConnectorStatusEnumType.Available,
+      } as OCPP20StatusNotificationRequest
+
+      // Act & Assert
+      assert.throws(
+        () => {
+          OCPP20ServiceUtils.buildStatusNotificationRequest(noEvseStation, input)
+        },
+        (error: unknown) => {
+          assert.ok(error instanceof OCPPError)
+          return true
+        }
+      )
+    })
+
+    await it('should include timestamp in response', () => {
+      const input = {
+        connectorId: 1,
+        connectorStatus: OCPP20ConnectorStatusEnumType.Occupied,
+        evseId: 1,
+      } as OCPP20StatusNotificationRequest
+
+      const result = OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input)
+
+      assert.ok(result.timestamp instanceof Date)
+    })
+
+    await it('should map connectorStatus correctly', () => {
+      const input = {
+        connectorId: 1,
+        connectorStatus: OCPP20ConnectorStatusEnumType.Occupied,
+        evseId: 1,
+      } as OCPP20StatusNotificationRequest
+
+      const result = OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input)
+
+      assert.strictEqual(result.connectorStatus, OCPP20ConnectorStatusEnumType.Occupied)
+    })
+
+    await it('should preserve connectorId in response', () => {
+      const input = {
+        connectorId: 2,
+        connectorStatus: OCPP20ConnectorStatusEnumType.Available,
+        evseId: 2,
+      } as OCPP20StatusNotificationRequest
+
+      const result = OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input)
+
+      assert.strictEqual(result.connectorId, 2)
+    })
+
+    await it('should accept status field as alias for connectorStatus', () => {
+      const input = {
+        connectorId: 1,
+        evseId: 1,
+        status: OCPP20ConnectorStatusEnumType.Faulted,
+      } as unknown as OCPP20StatusNotificationRequest
+
+      const result = OCPP20ServiceUtils.buildStatusNotificationRequest(mockStation, input)
+
+      assert.strictEqual(result.connectorStatus, OCPP20ConnectorStatusEnumType.Faulted)
+    })
+  })
+})
similarity index 89%
rename from tests/charging-station/ocpp/OCPPServiceUtils-StopTransaction.test.ts
rename to tests/charging-station/ocpp/OCPPServiceOperations.test.ts
index 73a6f368f7a303f0f2bd6ab8a11f71f7f79e16d8..4f99f56462112d586da88f6e6287c3cec3cb3060 100644 (file)
@@ -1,7 +1,8 @@
 /**
- * @file Tests for OCPPServiceUtils stop transaction functions
- * @description Verifies stopTransactionOnConnector and stopRunningTransactions
- *              version-dispatching functions
+ * @file Tests for OCPPServiceOperations version-dispatching functions
+ * @description Verifies startTransactionOnConnector, stopTransactionOnConnector,
+ *              stopRunningTransactions, and flushQueuedTransactionMessages
+ *              cross-version dispatchers
  */
 
 import assert from 'node:assert/strict'
@@ -16,12 +17,7 @@ import {
   stopRunningTransactions,
   stopTransactionOnConnector,
 } from '../../../src/charging-station/ocpp/OCPPServiceOperations.js'
-import { mapStopReasonToOCPP20 } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
-import {
-  type OCPP20TransactionEventRequest,
-  OCPPVersion,
-  type StopTransactionReason,
-} from '../../../src/types/index.js'
+import { type OCPP20TransactionEventRequest, OCPPVersion } from '../../../src/types/index.js'
 import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
 import { createMockChargingStation } from '../ChargingStationTestUtils.js'
 
@@ -85,7 +81,7 @@ function setupTransaction (
   connectorStatus.idTagAuthorized = true
 }
 
-await describe('OCPPServiceUtils — stop transaction functions', async () => {
+await describe('OCPPServiceOperations', async () => {
   afterEach(() => {
     standardCleanup()
   })
@@ -310,27 +306,4 @@ await describe('OCPPServiceUtils — stop transaction functions', async () => {
       assert.strictEqual(connectorStatus.transactionEventQueue.length, 0)
     })
   })
-
-  await describe('mapStopReasonToOCPP20', async () => {
-    await it('should map Other to Other/AbnormalCondition', () => {
-      const result = mapStopReasonToOCPP20('Other' as StopTransactionReason)
-
-      assert.strictEqual(result.stoppedReason, 'Other')
-      assert.strictEqual(result.triggerReason, 'AbnormalCondition')
-    })
-
-    await it('should map undefined to Local/StopAuthorized', () => {
-      const result = mapStopReasonToOCPP20(undefined)
-
-      assert.strictEqual(result.stoppedReason, 'Local')
-      assert.strictEqual(result.triggerReason, 'StopAuthorized')
-    })
-
-    await it('should map Remote to Remote/RemoteStop', () => {
-      const result = mapStopReasonToOCPP20('Remote' as StopTransactionReason)
-
-      assert.strictEqual(result.stoppedReason, 'Remote')
-      assert.strictEqual(result.triggerReason, 'RemoteStop')
-    })
-  })
 })
index 236284dd148138359baa88a841bf8fcecab66f7b..15b3eeb37ddac52ec4c7695d71be19e9153463c9 100644 (file)
@@ -5,7 +5,8 @@
  * Covers:
  * - ajvErrorsToErrorType — maps AJV validation errors to OCPP ErrorType
  * - convertDateToISOString — recursively converts Date objects to ISO strings in-place
- * - OCPPServiceUtils.isConnectorIdValid — validates connector ID ranges
+ * - isConnectorIdValid — validates connector ID ranges
+ * - mapStopReasonToOCPP20 — maps OCPP 1.6 stop reasons to OCPP 2.0 equivalents
  */
 
 import type { ErrorObject } from 'ajv'
@@ -19,8 +20,14 @@ import {
   ajvErrorsToErrorType,
   convertDateToISOString,
   isConnectorIdValid,
+  mapStopReasonToOCPP20,
 } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
-import { ErrorType, IncomingRequestCommand, type JsonType } from '../../../src/types/index.js'
+import {
+  ErrorType,
+  IncomingRequestCommand,
+  type JsonType,
+  type StopTransactionReason,
+} from '../../../src/types/index.js'
 import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
 
 /**
@@ -162,4 +169,27 @@ await describe('OCPPServiceUtils — pure functions', async () => {
       assert.strictEqual(result, false)
     })
   })
+
+  await describe('mapStopReasonToOCPP20', async () => {
+    await it('should map Other to Other/AbnormalCondition', () => {
+      const result = mapStopReasonToOCPP20('Other' as StopTransactionReason)
+
+      assert.strictEqual(result.stoppedReason, 'Other')
+      assert.strictEqual(result.triggerReason, 'AbnormalCondition')
+    })
+
+    await it('should map undefined to Local/StopAuthorized', () => {
+      const result = mapStopReasonToOCPP20(undefined)
+
+      assert.strictEqual(result.stoppedReason, 'Local')
+      assert.strictEqual(result.triggerReason, 'StopAuthorized')
+    })
+
+    await it('should map Remote to Remote/RemoteStop', () => {
+      const result = mapStopReasonToOCPP20('Remote' as StopTransactionReason)
+
+      assert.strictEqual(result.stoppedReason, 'Remote')
+      assert.strictEqual(result.triggerReason, 'RemoteStop')
+    })
+  })
 })