]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ocpp): complete version-separation in OCPPServiceUtils
authorJérôme Benoit <jerome.benoit@sap.com>
Wed, 1 Apr 2026 17:53:28 +0000 (19:53 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Wed, 1 Apr 2026 17:53:28 +0000 (19:53 +0200)
Replace buildSampledValue version-switch with resolveSampledValueFields
helper + version-specific buildOCPP16SampledValue/buildOCPP20SampledValue.
Move mapStopReasonToOCPP20 to OCPP20RequestBuilders where it belongs.

OCPPServiceUtils.ts is now 100% version-agnostic — zero OCPP 1.6/2.0
types, zero inline version switches. All version-specific logic lives
in version-specific leaf modules (RequestBuilders, ServiceUtils).

src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/OCPPServiceOperations.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts

index 13812bd3c42bc39b82ddc05f696febf48198a9fd..7beb3181b99ea6abd742fc4c67cf01e066e578f8 100644 (file)
@@ -14,7 +14,6 @@ import {
   type OCPP16BootNotificationRequest,
   type OCPP16MeterValue,
   type OCPP16SampledValue,
-  OCPPVersion,
   RequestCommand,
   type SampledValueTemplate,
 } from '../../../types/index.js'
@@ -27,9 +26,9 @@ import {
   buildEmptyMeterValue,
   buildEnergyMeasurandValue,
   buildPowerMeasurandValue,
-  buildSampledValue,
   buildSocMeasurandValue,
   buildVoltageMeasurandValue,
+  resolveSampledValueFields,
   updateConnectorEnergyValues,
   validateCurrentMeasurandPhaseValue,
   validateCurrentMeasurandValue,
@@ -319,11 +318,13 @@ export function buildOCPP16SampledValue (
   context?: MeterValueContext,
   phase?: MeterValuePhase
 ): OCPP16SampledValue {
-  return buildSampledValue(
-    OCPPVersion.VERSION_16,
-    sampledValueTemplate,
-    value,
-    context,
-    phase
-  ) as OCPP16SampledValue
+  const fields = resolveSampledValueFields(sampledValueTemplate, value, context, phase)
+  return {
+    context: fields.context,
+    location: fields.location,
+    measurand: fields.measurand,
+    unit: fields.unit,
+    value: fields.value.toString(),
+    ...(fields.phase != null && { phase: fields.phase }),
+  } as OCPP16SampledValue
 }
index 1d73ce05c15c8367ee50878c45f18ce320eabce7..f5f21524fe8c0d8b559a9ddd0d25e9c71687804b 100644 (file)
@@ -34,7 +34,6 @@ import {
   OCPP16MeterValueMeasurand,
   OCPP16MeterValueUnit,
   OCPP16RequestCommand,
-  type OCPP16SampledValue,
   OCPP16StandardParametersKey,
   type OCPP16StatusNotificationRequest,
   OCPP16StopTransactionReason,
@@ -61,12 +60,12 @@ import { sendAndSetConnectorStatus } from '../OCPPConnectorStatusOperations.js'
 import {
   buildEmptyMeterValue,
   buildMeterValue,
-  buildSampledValue,
   createPayloadConfigs,
   getSampledValueTemplate,
   PayloadValidatorOptions,
 } from '../OCPPServiceUtils.js'
 import { OCPP16Constants } from './OCPP16Constants.js'
+import { buildOCPP16SampledValue } from './OCPP16RequestBuilders.js'
 
 const moduleName = 'OCPP16ServiceUtils'
 
@@ -141,12 +140,11 @@ export class OCPP16ServiceUtils {
       const unitDivider =
         sampledValueTemplate.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
       meterValue.sampledValue.push(
-        buildSampledValue(
-          chargingStation.stationInfo?.ocppVersion,
+        buildOCPP16SampledValue(
           sampledValueTemplate,
           roundTo((meterStart ?? 0) / unitDivider, 4),
           OCPP16MeterValueContext.TRANSACTION_BEGIN
-        ) as OCPP16SampledValue
+        )
       )
     }
     return meterValue
@@ -188,12 +186,11 @@ export class OCPP16ServiceUtils {
     const unitDivider = sampledValueTemplate.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
     const meterValue = buildEmptyMeterValue() as OCPP16MeterValue
     meterValue.sampledValue.push(
-      buildSampledValue(
-        OCPPVersion.VERSION_16,
+      buildOCPP16SampledValue(
         sampledValueTemplate,
         roundTo((meterStop ?? 0) / unitDivider, 4),
         OCPP16MeterValueContext.TRANSACTION_END
-      ) as OCPP16SampledValue
+      )
     )
     return meterValue
   }
index 6289bf8438b949d01adcf356714b86bf93291f28..901b43ddf4dbac4862823b8730356bed88a7fbd2 100644 (file)
@@ -1,4 +1,5 @@
 import type { ChargingStation } from '../../../charging-station/index.js'
+import type { StopTransactionReason } from '../../../types/index.js'
 
 import { OCPPError } from '../../../exception/index.js'
 import {
@@ -9,10 +10,12 @@ import {
   type MeterValueContext,
   type MeterValuePhase,
   MeterValueUnit,
+  OCPP16StopTransactionReason,
   type OCPP20BootNotificationRequest,
   type OCPP20MeterValue,
+  OCPP20ReasonEnumType,
   type OCPP20SampledValue,
-  OCPPVersion,
+  OCPP20TriggerReasonEnumType,
   RequestCommand,
   type SampledValueTemplate,
 } from '../../../types/index.js'
@@ -25,9 +28,9 @@ import {
   buildEmptyMeterValue,
   buildEnergyMeasurandValue,
   buildPowerMeasurandValue,
-  buildSampledValue,
   buildSocMeasurandValue,
   buildVoltageMeasurandValue,
+  resolveSampledValueFields,
   updateConnectorEnergyValues,
   validateEnergyMeasurandValue,
   validateSocMeasurandValue,
@@ -228,11 +231,81 @@ export function buildOCPP20SampledValue (
   context?: MeterValueContext,
   phase?: MeterValuePhase
 ): OCPP20SampledValue {
-  return buildSampledValue(
-    OCPPVersion.VERSION_20,
-    sampledValueTemplate,
-    value,
-    context,
-    phase
-  ) as OCPP20SampledValue
+  const fields = resolveSampledValueFields(sampledValueTemplate, value, context, phase)
+  return {
+    context: fields.context,
+    location: fields.location,
+    measurand: fields.measurand,
+    ...(fields.unit !== undefined && { unitOfMeasure: { unit: fields.unit } }),
+    value: fields.value,
+    ...(fields.phase != null && { phase: fields.phase }),
+  } as OCPP20SampledValue
+}
+
+export const mapStopReasonToOCPP20 = (
+  reason?: StopTransactionReason
+): {
+  stoppedReason: OCPP20ReasonEnumType
+  triggerReason: OCPP20TriggerReasonEnumType
+} => {
+  switch (reason) {
+    case OCPP16StopTransactionReason.DE_AUTHORIZED:
+    case OCPP20ReasonEnumType.DeAuthorized:
+      return {
+        stoppedReason: OCPP20ReasonEnumType.DeAuthorized,
+        triggerReason: OCPP20TriggerReasonEnumType.Deauthorized,
+      }
+    case OCPP16StopTransactionReason.EMERGENCY_STOP:
+    case OCPP20ReasonEnumType.EmergencyStop:
+      return {
+        stoppedReason: OCPP20ReasonEnumType.EmergencyStop,
+        triggerReason: OCPP20TriggerReasonEnumType.AbnormalCondition,
+      }
+    case OCPP16StopTransactionReason.EV_DISCONNECTED:
+    case OCPP20ReasonEnumType.EVDisconnected:
+      return {
+        stoppedReason: OCPP20ReasonEnumType.EVDisconnected,
+        triggerReason: OCPP20TriggerReasonEnumType.EVDeparted,
+      }
+    case OCPP16StopTransactionReason.HARD_RESET:
+    case OCPP16StopTransactionReason.REBOOT:
+    case OCPP16StopTransactionReason.SOFT_RESET:
+    case OCPP20ReasonEnumType.ImmediateReset:
+    case OCPP20ReasonEnumType.Reboot:
+      return {
+        stoppedReason: OCPP20ReasonEnumType.ImmediateReset,
+        triggerReason: OCPP20TriggerReasonEnumType.ResetCommand,
+      }
+    case OCPP16StopTransactionReason.OTHER:
+    case OCPP20ReasonEnumType.Other:
+      return {
+        stoppedReason: OCPP20ReasonEnumType.Other,
+        triggerReason: OCPP20TriggerReasonEnumType.AbnormalCondition,
+      }
+    case OCPP16StopTransactionReason.POWER_LOSS:
+    case OCPP20ReasonEnumType.PowerLoss:
+      return {
+        stoppedReason: OCPP20ReasonEnumType.PowerLoss,
+        triggerReason: OCPP20TriggerReasonEnumType.AbnormalCondition,
+      }
+    case OCPP16StopTransactionReason.REMOTE:
+    case OCPP20ReasonEnumType.Remote:
+      return {
+        stoppedReason: OCPP20ReasonEnumType.Remote,
+        triggerReason: OCPP20TriggerReasonEnumType.RemoteStop,
+      }
+    case OCPP20ReasonEnumType.TimeLimitReached:
+      return {
+        stoppedReason: OCPP20ReasonEnumType.TimeLimitReached,
+        triggerReason: OCPP20TriggerReasonEnumType.TimeLimitReached,
+      }
+    case OCPP16StopTransactionReason.LOCAL:
+    case OCPP20ReasonEnumType.Local:
+    case undefined:
+    default:
+      return {
+        stoppedReason: OCPP20ReasonEnumType.Local,
+        triggerReason: OCPP20TriggerReasonEnumType.StopAuthorized,
+      }
+  }
 }
index 88fd4b7c0efc6f053c21eb48251ad7156dc283ab..f8245d0ac6893dd8e681c80d1e0d02b2cafb576d 100644 (file)
@@ -58,9 +58,9 @@ import { sendAndSetConnectorStatus } from '../OCPPConnectorStatusOperations.js'
 import {
   buildMeterValue,
   createPayloadConfigs,
-  mapStopReasonToOCPP20,
   PayloadValidatorOptions,
 } from '../OCPPServiceUtils.js'
+import { mapStopReasonToOCPP20 } from './OCPP20RequestBuilders.js'
 import { OCPP20VariableManager } from './OCPP20VariableManager.js'
 
 const moduleName = 'OCPP20ServiceUtils'
index 88495b1c6b6831ceb008e04f4f91605324dea9d5..6f78b481e61b3f5285cf6dbf8e92efa2262a3763 100644 (file)
@@ -11,6 +11,7 @@ import {
 } from '../../types/index.js'
 import { logger, truncateId } from '../../utils/index.js'
 import { OCPP16ServiceUtils } from './1.6/OCPP16ServiceUtils.js'
+import { mapStopReasonToOCPP20 } from './2.0/OCPP20RequestBuilders.js'
 import { OCPP20ServiceUtils } from './2.0/OCPP20ServiceUtils.js'
 import {
   AuthContext,
@@ -19,7 +20,6 @@ import {
   IdentifierType,
   OCPPAuthServiceFactory,
 } from './auth/index.js'
-import { mapStopReasonToOCPP20 } from './OCPPServiceUtils.js'
 
 /**
  * Starts a transaction on a specific connector using the appropriate OCPP version handler.
index a9c76f6bbaca5a423e023b3bb9aa8a9da1f18dca..4cedebb0203c642bbc79cb24cfaa7edff7c50045 100644 (file)
@@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs'
 import { dirname, join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 
-import type { BootReasonEnumType, StopTransactionReason } from '../../types/index.js'
+import type { BootReasonEnumType } from '../../types/index.js'
 
 import { type ChargingStation, getConfigurationKey } from '../../charging-station/index.js'
 import { BaseError, OCPPError } from '../../exception/index.js'
@@ -29,11 +29,6 @@ import {
   MeterValueMeasurand,
   MeterValuePhase,
   MeterValueUnit,
-  type OCPP16SampledValue,
-  OCPP16StopTransactionReason,
-  OCPP20ReasonEnumType,
-  type OCPP20SampledValue,
-  OCPP20TriggerReasonEnumType,
   OCPPVersion,
   RequestCommand,
   type SampledValue,
@@ -109,79 +104,6 @@ export const buildBootNotificationRequest = (
   }
 }
 
-/**
- * Maps an OCPP 1.6 or generic stop transaction reason to OCPP 2.0 stopped and trigger reasons.
- * @param reason - Stop transaction reason to map
- * @returns Object containing the OCPP 2.0 stoppedReason and triggerReason
- */
-export const mapStopReasonToOCPP20 = (
-  reason?: StopTransactionReason
-): {
-  stoppedReason: OCPP20ReasonEnumType
-  triggerReason: OCPP20TriggerReasonEnumType
-} => {
-  switch (reason) {
-    case OCPP16StopTransactionReason.DE_AUTHORIZED:
-    case OCPP20ReasonEnumType.DeAuthorized:
-      return {
-        stoppedReason: OCPP20ReasonEnumType.DeAuthorized,
-        triggerReason: OCPP20TriggerReasonEnumType.Deauthorized,
-      }
-    case OCPP16StopTransactionReason.EMERGENCY_STOP:
-    case OCPP20ReasonEnumType.EmergencyStop:
-      return {
-        stoppedReason: OCPP20ReasonEnumType.EmergencyStop,
-        triggerReason: OCPP20TriggerReasonEnumType.AbnormalCondition,
-      }
-    case OCPP16StopTransactionReason.EV_DISCONNECTED:
-    case OCPP20ReasonEnumType.EVDisconnected:
-      return {
-        stoppedReason: OCPP20ReasonEnumType.EVDisconnected,
-        triggerReason: OCPP20TriggerReasonEnumType.EVDeparted,
-      }
-    case OCPP16StopTransactionReason.HARD_RESET:
-    case OCPP16StopTransactionReason.REBOOT:
-    case OCPP16StopTransactionReason.SOFT_RESET:
-    case OCPP20ReasonEnumType.ImmediateReset:
-    case OCPP20ReasonEnumType.Reboot:
-      return {
-        stoppedReason: OCPP20ReasonEnumType.ImmediateReset,
-        triggerReason: OCPP20TriggerReasonEnumType.ResetCommand,
-      }
-    case OCPP16StopTransactionReason.OTHER:
-    case OCPP20ReasonEnumType.Other:
-      return {
-        stoppedReason: OCPP20ReasonEnumType.Other,
-        triggerReason: OCPP20TriggerReasonEnumType.AbnormalCondition,
-      }
-    case OCPP16StopTransactionReason.POWER_LOSS:
-    case OCPP20ReasonEnumType.PowerLoss:
-      return {
-        stoppedReason: OCPP20ReasonEnumType.PowerLoss,
-        triggerReason: OCPP20TriggerReasonEnumType.AbnormalCondition,
-      }
-    case OCPP16StopTransactionReason.REMOTE:
-    case OCPP20ReasonEnumType.Remote:
-      return {
-        stoppedReason: OCPP20ReasonEnumType.Remote,
-        triggerReason: OCPP20TriggerReasonEnumType.RemoteStop,
-      }
-    case OCPP20ReasonEnumType.TimeLimitReached:
-      return {
-        stoppedReason: OCPP20ReasonEnumType.TimeLimitReached,
-        triggerReason: OCPP20TriggerReasonEnumType.TimeLimitReached,
-      }
-    case OCPP16StopTransactionReason.LOCAL:
-    case OCPP20ReasonEnumType.Local:
-    case undefined:
-    default:
-      return {
-        stoppedReason: OCPP20ReasonEnumType.Local,
-        triggerReason: OCPP20TriggerReasonEnumType.StopAuthorized,
-      }
-  }
-}
-
 /**
  * Converts Ajv validation errors to the corresponding OCPP error type.
  * @param errors - Array of Ajv validation error objects
@@ -1254,59 +1176,42 @@ export const getSampledValueTemplate = (
 }
 
 /**
- * Builds a sampled value object according to the specified OCPP version.
- * @param ocppVersion - The OCPP version to use for formatting the sampled value
+ * Resolves the common sampled value fields from a template and optional overrides.
  * @param sampledValueTemplate - Template containing measurement configuration and metadata
  * @param value - The measured numeric value to be included in the sampled value
  * @param context - Optional context specifying when the measurement was taken (e.g., Sample.Periodic)
  * @param phase - Optional phase information for multi-phase electrical measurements
- * @returns A sampled value object formatted according to the specified OCPP version
+ * @returns An object containing the resolved sampled value fields
  */
-export function buildSampledValue (
-  ocppVersion: OCPPVersion | undefined,
+export const resolveSampledValueFields = (
   sampledValueTemplate: SampledValueTemplate,
   value: number,
   context?: MeterValueContext,
   phase?: MeterValuePhase
-): SampledValue {
-  const sampledValueMeasurand = sampledValueTemplate.measurand ?? getMeasurandDefault()
-  const sampledValueUnit =
-    sampledValueTemplate.unit ?? getMeasurandDefaultUnit(sampledValueMeasurand)
-  const sampledValueContext =
-    context ?? sampledValueTemplate.context ?? getMeasurandDefaultContext(sampledValueMeasurand)
-  const sampledValueLocation =
-    sampledValueTemplate.location ?? getMeasurandDefaultLocation(sampledValueMeasurand)
-  const sampledValuePhase = phase ?? sampledValueTemplate.phase
-
-  switch (ocppVersion) {
-    case OCPPVersion.VERSION_16:
-      // OCPP 1.6 format
-      return {
-        context: sampledValueContext,
-        location: sampledValueLocation,
-        measurand: sampledValueMeasurand,
-        unit: sampledValueUnit,
-        value: value.toString(), // OCPP 1.6 uses string
-        ...(sampledValuePhase != null && { phase: sampledValuePhase }),
-      } as OCPP16SampledValue
-    case OCPPVersion.VERSION_20:
-    case OCPPVersion.VERSION_201:
-      // OCPP 2.0 format
-      return {
-        context: sampledValueContext,
-        location: sampledValueLocation,
-        measurand: sampledValueMeasurand,
-        ...(sampledValueUnit !== undefined && { unitOfMeasure: { unit: sampledValueUnit } }),
-        value, // OCPP 2.0 uses number
-        ...(sampledValuePhase != null && { phase: sampledValuePhase }),
-      } as OCPP20SampledValue
-    default:
-      throw new OCPPError(
-        ErrorType.INTERNAL_ERROR,
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        `Cannot build sampledValue: OCPP version ${ocppVersion} not supported`,
-        RequestCommand.METER_VALUES
-      )
+): {
+  context: MeterValueContext
+  location: MeterValueLocation | undefined
+  measurand: MeterValueMeasurand
+  phase: MeterValuePhase | undefined
+  unit: MeterValueUnit | undefined
+  value: number
+} => {
+  const sampledValueMeasurand =
+    (sampledValueTemplate.measurand as MeterValueMeasurand | undefined) ?? getMeasurandDefault()
+  return {
+    context:
+      context ??
+      (sampledValueTemplate.context as MeterValueContext | undefined) ??
+      getMeasurandDefaultContext(sampledValueMeasurand),
+    location:
+      (sampledValueTemplate.location as MeterValueLocation | undefined) ??
+      getMeasurandDefaultLocation(sampledValueMeasurand),
+    measurand: sampledValueMeasurand,
+    phase: phase ?? (sampledValueTemplate.phase as MeterValuePhase | undefined),
+    unit:
+      (sampledValueTemplate.unit as MeterValueUnit | undefined) ??
+      getMeasurandDefaultUnit(sampledValueMeasurand),
+    value,
   }
 }
 
index 97ddf96e4ddf23302b1233c8f9b4d8227df4eaca..24688d1bc7e6f08df82a6a04695312fbe3555db8 100644 (file)
@@ -7,7 +7,7 @@
  * - buildBootNotificationRequest — builds version-specific boot notification payloads
  * - convertDateToISOString — recursively converts Date objects to ISO strings in-place
  * - isConnectorIdValid — validates connector ID ranges
- * - mapStopReasonToOCPP20 — maps OCPP 1.6 stop reasons to OCPP 2.0 equivalents
+ * - mapStopReasonToOCPP20 — maps OCPP 1.6 stop reasons to OCPP 2.0 equivalents (from OCPP20RequestBuilders)
  */
 
 import type { ErrorObject } from 'ajv'
@@ -17,12 +17,12 @@ import { afterEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../../src/charging-station/index.js'
 
+import { mapStopReasonToOCPP20 } from '../../../src/charging-station/ocpp/2.0/OCPP20RequestBuilders.js'
 import {
   ajvErrorsToErrorType,
   buildBootNotificationRequest,
   convertDateToISOString,
   isConnectorIdValid,
-  mapStopReasonToOCPP20,
 } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
 import {
   BootReasonEnumType,