From df3fe1db9f27a659b7586c010d658a33c7e982b6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 1 Apr 2026 19:53:28 +0200 Subject: [PATCH] refactor(ocpp): complete version-separation in OCPPServiceUtils MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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). --- .../ocpp/1.6/OCPP16RequestBuilders.ts | 19 +-- .../ocpp/1.6/OCPP16ServiceUtils.ts | 13 +- .../ocpp/2.0/OCPP20RequestBuilders.ts | 91 +++++++++-- .../ocpp/2.0/OCPP20ServiceUtils.ts | 2 +- .../ocpp/OCPPServiceOperations.ts | 2 +- src/charging-station/ocpp/OCPPServiceUtils.ts | 151 ++++-------------- .../ocpp/OCPPServiceUtils-pure.test.ts | 4 +- 7 files changed, 129 insertions(+), 153 deletions(-) diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts b/src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts index 13812bd3..7beb3181 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts @@ -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 } diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 1d73ce05..f5f21524 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -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 } diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts b/src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts index 6289bf84..901b43dd 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts @@ -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, + } + } } diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 88fd4b7c..f8245d0a 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -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' diff --git a/src/charging-station/ocpp/OCPPServiceOperations.ts b/src/charging-station/ocpp/OCPPServiceOperations.ts index 88495b1c..6f78b481 100644 --- a/src/charging-station/ocpp/OCPPServiceOperations.ts +++ b/src/charging-station/ocpp/OCPPServiceOperations.ts @@ -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. diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index a9c76f6b..4cedebb0 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -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, } } diff --git a/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts b/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts index 97ddf96e..24688d1b 100644 --- a/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts +++ b/tests/charging-station/ocpp/OCPPServiceUtils-pure.test.ts @@ -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, -- 2.53.0