* feat(ocpp16): wire signed meter values into trigger + broadcast paths (issue #1936 d)
OCPP 1.6 Signed Meter Values whitepaper v1.0 requires that when
SampledDataSignReadings + SampledDataSignUpdatedReadings are enabled
and a signing key is configured, a paired SignedData SampledValue
accompanies the Raw SampledValue in every Sample.Periodic-context
MeterValue. The periodic loop (OCPP16ServiceUtils.startUpdatedMeterValues)
already applies POST-HOC signing after buildMeterValue returns, so
periodic MeterValues are signed today.
Two callsites were missing the signing wrapper:
1. OCPP 1.6 TriggerMessage(MeterValues) handler in
OCPP16IncomingRequestService.ts (both the specific-connectorId
branch and the broadcast-to-all-connectors branch).
2. Worker broadcast channel in
ChargingStationWorkerBroadcastChannel.handleMeterValues
(cross-version handler; signing is 1.6-only per the whitepaper).
Both paths silently emitted unsigned Raw values instead of the
whitepaper-mandated paired SignedData SampledValue when signing was
enabled.
Fix: consolidate the signing block from startUpdatedMeterValues into
a new public static helper OCPP16ServiceUtils.appendSignedUpdatedReadings
that mutates the MeterValue in place. The helper is a no-op when
signing is disabled or the connector's signing config is missing.
Apply the helper at the three missing callsites and refactor
startUpdatedMeterValues to call it too.
Test invariant fail: 0, skipped: 6 preserved (2895 pass / 2901 total).
Spec citation: OCA Application Note 'Signed Meter Values for OCPP 1.6'
v1.0 §3.2.1 (paired SignedData SampledValue).
Closes issue #1936 item (d).
* docs(ocpp16): apply round-1 review fixes on signed MV wire-up (issue #1936 d)
Address cross-validated R1 findings:
- Naming coherence in TriggerMessage broadcast-to-all branch: rename
loop-local variables from abbreviated 'id'/'cs'/'txId'/'mv' to
full 'connectorId'/'connectorStatus'/'transactionId'/'meterValue'
to match the specific-connector sibling branch style. Per AGENTS.md
naming coherence: two adjacent branches doing the same thing with
different name styles is the 'synonym creates ambiguity' pattern.
- Rewrite the broadcast channel guard comment to correctly attribute
the '!isOcpp2' skip. Previous wording claimed 'the whitepaper is OCPP
1.6 specific', but the whitepaper §4 explicitly covers OCPP 2.x. The
actual reason for the skip is that OCPP 2.0.x signing is applied
inline inside buildMeterValue via the versioned dispatcher's signing
hook, so post-hoc wrapping is the 1.6 pattern only.
- Tighten the appendSignedUpdatedReadings @description to drop
refactor-history narrative ('Consolidates the signing block used by
every trigger/broadcast path...'). Replace with operational spec
citing whitepaper §3.3.6 SampledDataSignUpdatedReadings and the
mutation contract on connectorStatus.publicKeySentInTransaction
(per PublicKeyWithSignedMeterValue = OncePerTransaction).
* fix(ocpp16): harden signed MV helper and thread reading context (issue #1936 d)
Enforce the transactionStarted invariant inside `appendSignedUpdatedReadings` so callsites
with looser guards cannot leak signed emissions past `resetConnectorTransactionStatus`.
Accept an optional reading `context` parameter (default `Sample.Periodic`);
`TriggerMessage(MeterValues)` callsites pass `Trigger` per OCPP 1.6 Core Table 30 so the
signed payload's context field matches its emission source.
Downgrade the `readSigningConfigForConnector` disabled-state log from `warn` to `debug`;
the helper is a hot-path probe whose `undefined` return already conveys the disabled state
to callers.
Test invariant `fail: 0, skipped: 6` preserved (2925 pass / 2931 total).
type MeterValuesRequest,
type MeterValuesResponse,
type OCPP16AuthorizeResponse,
+ type OCPP16MeterValue,
OCPP20AuthorizationStatusEnumType,
type OCPP20AuthorizeResponse,
type OCPP20Get15118EVCertificateResponse,
logger,
} from '../../utils/index.js'
import { getConfigurationKey } from '../ConfigurationKeyUtils.js'
-import { buildMeterValue, OCPP20ServiceUtils, sendAndSetConnectorStatus } from '../ocpp/index.js'
+import {
+ buildMeterValue,
+ OCPP16ServiceUtils,
+ OCPP20ServiceUtils,
+ sendAndSetConnectorStatus,
+} from '../ocpp/index.js'
import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
const moduleName = 'ChargingStationWorkerBroadcastChannel'
? secondsToMilliseconds(convertToInt(key.value))
: Constants.DEFAULT_METER_VALUES_INTERVAL_MS
})()
+ const meterValue = buildMeterValue(this.chargingStation, transactionId, interval)
+ // OCPP 1.6 Signed Meter Values whitepaper §3.3.6: mirror the periodic
+ // loop (`OCPP16ServiceUtils.startUpdatedMeterValues`) and append a paired
+ // SignedData SampledValue when signing is enabled for the connector.
+ // Guarded on `!isOcpp2` because OCPP 2.0.x signing is applied inline
+ // inside `buildMeterValue` (via the versioned dispatcher's signing hook);
+ // post-hoc wrapping is the 1.6 pattern only. Guarded on `transactionId
+ // != null` because signing a MeterValue outside an active transaction
+ // has no defined semantics in the whitepaper.
+ if (!isOcpp2 && transactionId != null) {
+ OCPP16ServiceUtils.appendSignedUpdatedReadings(
+ this.chargingStation,
+ connectorId,
+ convertToInt(transactionId),
+ meterValue as OCPP16MeterValue
+ )
+ }
return await this.chargingStation.ocppRequestService.requestHandler<
MeterValuesRequest,
MeterValuesResponse
evseId: payloadEvseId ?? this.chargingStation.getEvseIdByConnectorId(connectorId),
}
: { connectorId }),
- meterValue: [buildMeterValue(this.chargingStation, transactionId, interval)],
+ meterValue: [meterValue],
...requestPayload,
} as MeterValuesRequest,
this.requestParams
OCPP16IncomingRequestCommand,
OCPP16MessageTrigger,
type OCPP16MeterValue,
+ OCPP16MeterValueContext,
type OCPP16MeterValuesRequest,
type OCPP16MeterValuesResponse,
OCPP16RequestCommand,
connectorStatus?.transactionStarted === true &&
connectorStatus.transactionId != null
) {
+ const transactionId = convertToInt(connectorStatus.transactionId)
+ const meterValue = buildMeterValue(
+ chargingStation,
+ transactionId,
+ 0
+ ) as OCPP16MeterValue
+ OCPP16ServiceUtils.appendSignedUpdatedReadings(
+ chargingStation,
+ connectorId,
+ transactionId,
+ meterValue,
+ OCPP16MeterValueContext.TRIGGER
+ )
chargingStation.ocppRequestService
.requestHandler<OCPP16MeterValuesRequest, OCPP16MeterValuesResponse>(
chargingStation,
OCPP16RequestCommand.METER_VALUES,
{
connectorId,
- meterValue: [
- buildMeterValue(
- chargingStation,
- convertToInt(connectorStatus.transactionId),
- 0
- ) as OCPP16MeterValue,
- ],
- transactionId: convertToInt(connectorStatus.transactionId),
+ meterValue: [meterValue],
+ transactionId,
},
{
triggerMessage: true,
.catch(errorHandler)
}
} else {
- for (let id = 1; id <= chargingStation.getNumberOfConnectors(); id++) {
- const cs = chargingStation.getConnectorStatus(id)
- if (cs?.transactionStarted === true && cs.transactionId != null) {
+ for (
+ let connectorId = 1;
+ connectorId <= chargingStation.getNumberOfConnectors();
+ connectorId++
+ ) {
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ if (
+ connectorStatus?.transactionStarted === true &&
+ connectorStatus.transactionId != null
+ ) {
+ const transactionId = convertToInt(connectorStatus.transactionId)
+ const meterValue = buildMeterValue(
+ chargingStation,
+ transactionId,
+ 0
+ ) as OCPP16MeterValue
+ OCPP16ServiceUtils.appendSignedUpdatedReadings(
+ chargingStation,
+ connectorId,
+ transactionId,
+ meterValue,
+ OCPP16MeterValueContext.TRIGGER
+ )
chargingStation.ocppRequestService
.requestHandler<OCPP16MeterValuesRequest, OCPP16MeterValuesResponse>(
chargingStation,
OCPP16RequestCommand.METER_VALUES,
{
- connectorId: id,
- meterValue: [
- buildMeterValue(
- chargingStation,
- convertToInt(cs.transactionId),
- 0
- ) as OCPP16MeterValue,
- ],
- transactionId: convertToInt(cs.transactionId),
+ connectorId,
+ meterValue: [meterValue],
+ transactionId,
},
{
triggerMessage: true,
[OCPP16RequestCommand.STOP_TRANSACTION, 'StopTransaction'],
]
+ /**
+ * Post-hoc signing wrapper for OCPP 1.6 Signed Meter Values whitepaper
+ * §3.3.6 (`SampledDataSignUpdatedReadings`). When
+ * `SampledDataSignReadings` and `SampledDataSignUpdatedReadings` are
+ * both enabled and a signing key is configured for the connector,
+ * appends a paired `SignedData` `SampledValue` to the supplied
+ * `MeterValue`. Idempotent no-op when signing is disabled or the
+ * signing prerequisites are absent.
+ *
+ * Mutates `meterValue.sampledValue` in place and updates
+ * `connectorStatus.publicKeySentInTransaction` so the public-key
+ * payload is emitted at most once per transaction (per
+ * `PublicKeyWithSignedMeterValue = OncePerTransaction`).
+ * @param chargingStation - Target charging station.
+ * @param connectorId - Connector identifier owning the transaction.
+ * @param transactionId - Active transaction identifier.
+ * @param meterValue - MeterValue to mutate (in place).
+ * @param context - Reading context for the emitted `SignedData` sampled value (defaults to `Sample.Periodic`); pass `Trigger` for TriggerMessage-originated emissions per OCPP 1.6 Core Table 30.
+ */
+ public static appendSignedUpdatedReadings (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ transactionId: number,
+ meterValue: OCPP16MeterValue,
+ context: OCPP16MeterValueContext = OCPP16MeterValueContext.SAMPLE_PERIODIC
+ ): void {
+ if (
+ !OCPP16ServiceUtils.isSigningEnabled(chargingStation) ||
+ !OCPP16ServiceUtils.isSigningUpdatedReadingsEnabled(chargingStation)
+ ) {
+ return
+ }
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ if (connectorStatus?.transactionStarted !== true) {
+ return
+ }
+ const signingCfg = OCPP16ServiceUtils.readSigningConfigForConnector(
+ chargingStation,
+ connectorId
+ )
+ if (signingCfg == null) {
+ return
+ }
+ const energyWh = chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId)
+ const publicKeySentInTransaction = connectorStatus.publicKeySentInTransaction ?? false
+ const signedResult = OCPP16ServiceUtils.buildSignedSampledValue(
+ signingCfg,
+ energyWh,
+ context,
+ transactionId,
+ publicKeySentInTransaction,
+ meterValue.timestamp
+ )
+ meterValue.sampledValue.push(signedResult.sampledValue)
+ if (signedResult.publicKeyIncluded) {
+ connectorStatus.publicKeySentInTransaction = true
+ }
+ }
+
/**
* @param commandParams - Status notification parameters
* @returns Formatted OCPP 1.6 StatusNotification request payload
}
connectorStatus.transactionUpdatedMeterValuesSetInterval = setInterval(() => {
const transactionId = convertToInt(connectorStatus.transactionId)
- const meterValue = buildMeterValue(chargingStation, transactionId, interval)
- if (
- OCPP16ServiceUtils.isSigningEnabled(chargingStation) &&
- OCPP16ServiceUtils.isSigningUpdatedReadingsEnabled(chargingStation)
- ) {
- const energyWh = chargingStation.getEnergyActiveImportRegisterByTransactionId(
- connectorStatus.transactionId
- )
- const publicKeySentInTransaction = connectorStatus.publicKeySentInTransaction ?? false
- const signingCfg = OCPP16ServiceUtils.readSigningConfigForConnector(
- chargingStation,
- connectorId
- )
- if (signingCfg != null) {
- const signedResult = OCPP16ServiceUtils.buildSignedSampledValue(
- signingCfg,
- energyWh,
- OCPP16MeterValueContext.SAMPLE_PERIODIC,
- transactionId,
- publicKeySentInTransaction,
- (meterValue as OCPP16MeterValue).timestamp
- )
- ;(meterValue as OCPP16MeterValue).sampledValue.push(signedResult.sampledValue)
- if (signedResult.publicKeyIncluded) {
- connectorStatus.publicKeySentInTransaction = true
- }
- }
- }
+ const meterValue = buildMeterValue(
+ chargingStation,
+ transactionId,
+ interval
+ ) as OCPP16MeterValue
+ OCPP16ServiceUtils.appendSignedUpdatedReadings(
+ chargingStation,
+ connectorId,
+ transactionId,
+ meterValue
+ )
chargingStation.ocppRequestService
.requestHandler<MeterValuesRequest, MeterValuesResponse>(
chargingStation,
connectorId,
meterValue: [meterValue],
transactionId,
- } as MeterValuesRequest
+ }
)
.catch((error: unknown) => {
logger.error(
const prerequisiteResult = validateSigningPrerequisites(publicKeyHex, configuredSigningMethod)
if (!prerequisiteResult.enabled) {
- logger.warn(
+ logger.debug(
`${chargingStation.logPrefix()} OCPP16ServiceUtils.readSigningConfigForConnector: Signed meter values disabled for connector ${connectorId.toString()}: ${prerequisiteResult.reason}`
)
return undefined