OCPP 2.0.1 F06.FR.06: when a CSMS sends a TriggerMessage with
MessageTrigger.MeterValues, the Charging Station SHALL respond with a
MeterValuesRequest carrying the requested Measurand values.
The current OCPP20IncomingRequestService.handleRequestTriggerMessage
handler for MessageTrigger.MeterValues iterated all connectors with
active transactions and sent TransactionEventRequest(Updated) — a
spec violation introduced by PR #1726. It only fell back to sending
a MeterValuesRequest when no active transactions existed.
Fix: always emit MeterValuesRequest for MessageTrigger.MeterValues.
- Iterate target EVSEs (specific from evse.id, or all if unspecified).
- Per EVSE, build one MeterValue per connector with an active
transaction using the OCPP 2.0.1 AlignedDataCtrlr.Measurands
allow-list and TRIGGER ReadingContext (matches F06 semantics for
aligned/triggered data). Sub-schema MeterValues with empty
sampledValue arrays are skipped per MeterValueType.sampledValue's
1..* cardinality.
- Emit one MeterValuesRequest per target EVSE aggregating those
MeterValues. When an EVSE has no active transactions, emit one
request with a schema-conforming placeholder so the CSMS observes
the response.
Test updates:
- Remove the MeterValues entry from the shared parameterized 'fires
requestHandler once' loop (that assertion baked in the pre-fix
1-call fallback behavior).
- Add a dedicated F06.FR.06 broadcast test asserting one
MeterValuesRequest per EVSE (3 EVSEs → 3 requests) — symmetric
with the existing StatusNotification broadcast test.
Test invariant fail: 0, skipped: 6 preserved.
Closes issue #1936 item (j).
* fix(ocpp20): tag trigger MV placeholder with Trigger context and measurand (issue #1936 j)
The placeholder SampledValue emitted when an EVSE has no active transaction
previously omitted both `context` and `measurand`, silently defaulting to
`Sample.Periodic` and `Energy.Active.Import.Register` per the OCPP 2.0.1
`MeterValuesRequest` schema. TC_F_12_CS (F06.FR.06 certification) asserts
`meterValue[0].sampledValue[0].context = 'Trigger'` on trigger-elicited
MeterValuesRequests, and reporting `Energy.Active.Import.Register = 0`
misleads the CSMS into recording a spurious energy register reset.
Sets `context = OCPP20ReadingContextEnumType.TRIGGER` and
`measurand = OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT` on the placeholder
so the wire payload is truthful (`0 W` when no charging is in progress)
and consistent with the surrounding trigger emission's declared context.
Extends the broadcast test to assert per-EVSE payload shape (command,
`evseId > 0`, `meterValue` non-empty, `sampledValue[0].context = Trigger`,
request options) mirroring the sibling StatusNotification broadcast test.
* test(ocpp20): use numeric comparator on trigger meterValues broadcast assertion (issue #1936 j)
`Array.prototype.sort()` sorts as strings by default; the coincidence with numeric
ordering only holds for the current fixture's single-digit EVSE ids `{1, 2, 3}`.
If a future fixture bumps `evsesCount` beyond 9, the assertion would silently
reorder (`['1','10','2','3']`) and mask a real bug.
Uses an explicit `(a, b) => a - b` numeric comparator so the sort behavior is
fixture-count-agnostic.
Extends the F06.FR.06 broadcast test to assert
`sampledValue[0].measurand = Power.Active.Import` on the placeholder path,
locking the design tradeoff: the placeholder emits
`Power.Active.Import = 0 W` (truthful when idle) rather than reading the
first entry from `AlignedDataCtrlr.Measurands` (whose default value
`Energy.Active.Import.Register = 0 Wh` would imply a cumulative register
reset).
F06.FR.09 requires "current information only"; a 0-value energy register
is a stronger lie than a 0-value power flow when no transaction is
running. The measurand assertion prevents a future regression that
swaps the placeholder for the schema-default measurand.
Harmonises the OCPP 2.0.1 TriggerMessage(MeterValues) case with the sibling
`triggerStatusNotification` / `triggerAllEvseStatusNotifications` pattern
already established in the same class. The 66-line inline case is replaced
by a single-line delegation, matching the visual rhythm of every other
switch case.
Design refinements enabled by the extraction:
- `emitEvseMeterValues` now consumes the `evseStatus` yielded directly by
`iterateEvses(true)`, removing a redundant Map lookup on every broadcast
target (matches the sibling `triggerAllEvseStatusNotifications` idiom).
- The unreachable `evseStatus == null` guard in the broadcast path is
eliminated; the type is narrowed once at the top of the specific-EVSE
branch.
- The F06.FR.06 spec-citation comment block is lifted to the helper's
JSDoc, composing F06.FR.10 (Accepted messages SHALL be sent) and
F06.FR.11 (evse absent -> broadcast to all EVSEs) references.
* fix(ocpp20): guard buildMeterValue against synchronous throw in trigger path (issue #1936 j)
`buildMeterValue` can throw synchronously - the sibling
`OCPP20ServiceUtils.buildTransactionStartedMeterValues` (lines 165-183)
wraps it in try/catch and logs on failure. `emitEvseMeterValues` was
calling the same builder unguarded; a synchronous throw would have
unwound through the EventEmitter listener without ever reaching the
downstream `.catch(errorHandler)` (which only observes Promise
rejection).
Mirrors the sibling defensive pattern: wraps the per-connector
`buildMeterValue` call in try/catch, logs the error with the module
scope name and forwarded reason, and continues the outer loop so that
one bad connector does not prevent the remaining connectors' meter
values from being aggregated into the outgoing MeterValuesRequest.