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