]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
fix(ocpp20): trigger meterValues path emits MeterValuesRequest per F06.FR.06 (issue...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sat, 4 Jul 2026 16:36:19 +0000 (18:36 +0200)
committerGitHub <noreply@github.com>
Sat, 4 Jul 2026 16:36:19 +0000 (18:36 +0200)
commit66890b06db7bf33b2c63520b6c7d0122a0b50b83
tree9c00706af253f79504ca243d0590c2c04e09b302
parentca08a84ca35370638dc5a1877fcc5e60fc08650b
fix(ocpp20): trigger meterValues path emits MeterValuesRequest per F06.FR.06 (issue #1936 j) (#1943)

* fix(ocpp20): trigger meterValues path emits MeterValuesRequest per F06.FR.06 (issue #1936 j)

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 invariant `fail: 0, skipped: 6` preserved (2925 pass / 2931 total).

* 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.

Test invariant `fail: 0, skipped: 6` preserved (2925 pass / 2931 total).

* test(ocpp20): lock trigger meterValues placeholder measurand value (issue #1936 j)

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.

Test invariant `fail: 0, skipped: 6` preserved (2925 pass / 2931 total).

* refactor(ocpp20): extract triggerMeterValues + emitEvseMeterValues helpers (issue #1936 j)

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.

Test invariant `fail: 0, skipped: 6` preserved (2916 pass / 2922 total).

* 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.

Test invariant `fail: 0, skipped: 6` preserved (2925 pass / 2931 total).
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts