]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(meter-values): honor RegisterValuesWithoutPhases in coherent path (issue #1936...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sat, 4 Jul 2026 22:21:37 +0000 (00:21 +0200)
committerGitHub <noreply@github.com>
Sat, 4 Jul 2026 22:21:37 +0000 (00:21 +0200)
commit2cbd92dd0ada34cc10982629fae91cac2e165593
treeefba45d67d747c7d9d2feb2a0b7a0b9495099e99
parent11cc0a37c7c72640a1fb5c5bc561a71ef3b4241d
feat(meter-values): honor RegisterValuesWithoutPhases in coherent path (issue #1936 k) (#1942)

* feat(meter-values): honor RegisterValuesWithoutPhases in coherent path (issue #1936 k)

OCPP 2.0.1 `SampledDataCtrlr.RegisterValuesWithoutPhases` suppresses
per-phase L-N emission of `Energy.Active.Import.Register` when set to
`true`. The coherent path previously emitted per-phase register based
solely on the connector template's phase qualifier, ignoring this
config variable (documented at `CoherentMeterValueBuilder.ts` L-N
branch as "not consulted").

Wire the variable through:
- Add `OCPP20OptionalVariableName.RegisterValuesWithoutPhases` to the
  OCPP 2.0.1 optional-variable enum (registry entry already existed).
- Extend `buildCoherentMeterValue` with an optional
  `registerValuesWithoutPhases` parameter (defaults to `false` when
  omitted, preserving current behavior for OCPP 1.6 callers).
- Extend `resolvePhasedValue` with the same flag; the
  `ENERGY_ACTIVE_IMPORT_REGISTER` L-N branch returns `undefined` when
  the flag is `true` so the caller log-and-skips the per-phase
  template, leaving only the aggregate register to emit.
- Wire the flag resolution from `OCPPServiceUtils.buildMeterValue`'s
  coherent strategy gate using the existing `isOCPP20FlagEnabled`
  helper. Safe on OCPP 1.6 stations because the component-scoped
  configuration key never resolves there (returns `false`, preserving
  current behavior).
- Move `OCPP20OptionalVariableName` from the type-only import block
  to the value import block in `OCPPServiceUtils.ts` (previously
  imported as type only; now used as a runtime value).

Three tests added exercising the new behavior:
- L-N per-phase `Energy.Active.Import.Register` templates suppressed
  and only the aggregate register emitted when the flag is `true`.
- Default per-phase L-N emission preserved when the flag is unset.
- `Power.Active.Import` per-phase emission unaffected (flag scoped to
  `Energy.Active.Import.Register`).

README section on coherent-mode phase emission updated to reflect the
new semantics.

Test invariant `fail: 0, skipped: 6` preserved.

Spec citation: OCPP 2.0.1 edition 3 part 2, `SampledDataCtrlr`
component: "If this variable reports a value of true, then meter
values of measurand `Energy.Active.Import.Register` will only report
the total energy over all phases without reporting the individual
phase values."

Closes issue #1936 item (k).

* docs(meter-values): refine OCPP 1.6 safety wire-comment wording (issue #1936 k)

The wire-site comment in `OCPPServiceUtils.ts` at the coherent strategy
gate previously stated:

> "OCPP 1.6 stations never resolve the component-scoped key (returns
> false), preserving current behavior."

`isOCPP20FlagEnabled` reads the raw configuration store via
`getConfigurationKey`. An OCPP 1.6 station whose template literally
set a key named `SampledDataCtrlr.RegisterValuesWithoutPhases` to
`"true"` WOULD resolve it. In practice this is impossible under normal
use (the key is not standard for 1.6), so the safety outcome is
correct, but the "never resolve" wording is not strictly accurate.

Weakened to: "OCPP 1.6 stations do not carry the component-scoped key
by default: `getConfigurationKey` returns `undefined`,
`convertToBoolean(undefined) === false`, so current behavior is
preserved."

Same intent (documenting OCPP 1.6 safety), more precise wording
matching the actual semantic of `isOCPP20FlagEnabled`.

No code change. Test invariant `fail: 0, skipped: 6` preserved.

* refactor(meter-values): apply RegisterValuesWithoutPhases at bucket level (issue #1936 k)

R2 Lane B found two spec-conformance issues in the initial threaded
implementation of `SampledDataCtrlr.RegisterValuesWithoutPhases`:

1. When only per-phase L-N `Energy.Active.Import.Register` templates are
   configured on a connector and the flag is `true`, the previous
   implementation suppressed all three L-N templates and emitted
   nothing for the measurand. This violates the spec:
   > "will only report the total energy over all phases without
   > reporting the individual phase values."
   The spec mandates that the total IS reported; it does not permit a
   silent no-op when the config lacks an aggregate template.

2. `resolvePhasedValue` returned `undefined` for suppressed L-N
   templates, and `buildCoherentMeterValue`'s log-and-skip path treated
   this as an unsupported `(measurand, phase)` combination, emitting a
   spurious `WARN` for every configured L-N register template on
   OCPP 2.0.1 stations with the flag set. This is a legitimate,
   config-driven skip, not an error condition.

Redesign the suppression as a bucket-level pre-filter in
`buildCoherentMeterValue` (before the emit loop) instead of a
per-call return-value negotiation with `resolvePhasedValue`. Both
issues collapse to zero:

- The new helper `applyRegisterValuesWithoutPhases` filters L-N
  templates out of the `Energy.Active.Import.Register` bucket before
  iteration.
- When the connector configured only per-phase L-N templates (no
  aggregate), the helper synthesizes an aggregate template from the
  first suppressed L-N by shallow-cloning it with `phase` cleared;
  `unit`, `measurand`, `location`, and `context` fields inherit from
  the L-N template so the emitted aggregate matches the operator's
  intent for encoding and scale.
- Because L-N templates are removed before iteration, the
  log-and-skip path fires only for genuinely unsupported combinations,
  never for a configured spec suppression.

`resolvePhasedValue` reverts to its pre-`a46c0189` signature (5
parameters, no `suppressPerPhaseRegister` flag) since the flag is now
handled entirely at the bucket boundary. The exported
`buildCoherentMeterValue` retains its `registerValuesWithoutPhases`
parameter and the strategy-gate wire in `OCPPServiceUtils.buildMeterValue`
is unchanged.

One test added exercising the synthesis path:
- `should synthesize aggregate Energy.Active.Import.Register when only
  per-phase L-N templates configured and registerValuesWithoutPhases=true`
  asserts 1 sample survives, the surviving sample has no phase
  qualifier, the value equals the total register (not
  `register / phases`), and the synthesized template inherits the unit
  from the first suppressed L-N.

README updated to describe the pre-filter + synthesis semantic and to
weaken the OCPP 1.6 wording (`getConfigurationKey` returns `undefined`,
`convertToBoolean(undefined) === false`) matching the `4dcb6213`
comment fix.

Test invariant `fail: 0, skipped: 6` preserved.

Spec citation: OCPP 2.0.1 edition 3 part 2, `SampledDataCtrlr`,
`RegisterValuesWithoutPhases`.

* test(meter-values): add OCPP 2.0.1 boundary tests for RegisterValuesWithoutPhases (issue #1936 k)

R5 (maintainer perspective) flagged missing OCPP 2.0 service-boundary
regression tests: the existing suite covered `buildCoherentMeterValue`
directly but not the `buildMeterValue` strategy gate that resolves the
`SampledDataCtrlr.RegisterValuesWithoutPhases` variable and threads it
into the coherent builder on an OCPP 2.0.1 station.

Two tests added to `StrategyDispatch.test.ts`:

1. `should synthesize aggregate register at buildMeterValue boundary
   when the SampledDataCtrlr variable resolves to true`. Uses
   `addConfigurationKey(station, buildConfigKey(...), 'true')` to set
   the component-scoped key exactly as the runtime does, injects a
   3-phase coherent session, configures only per-phase L-N Energy
   templates, calls `buildMeterValue`, and asserts exactly one
   synthesized aggregate sample survives with no phase qualifier.

2. `should preserve per-phase L-N emission when the SampledDataCtrlr
   variable is absent`. Same setup without the configuration key;
   asserts all 3 per-phase L-N samples emit (default behavior
   preserved when the variable does not resolve).

These tests exercise the full wire path: `isOCPP20FlagEnabled` reads
the config store, threads the boolean into `buildCoherentMeterValue`'s
`registerValuesWithoutPhases` parameter, which triggers
`applyRegisterValuesWithoutPhases` bucket pre-filtering and (when
appropriate) aggregate synthesis.

Test invariant `fail: 0, skipped: 6` preserved.

* refactor(meter-values): partition RegisterValuesWithoutPhases by identity family (issue #1936 k)

R8 Lane B (OCPP spec + physics + math adversarial review) surfaced a
correctness gap in the previous `applyRegisterValuesWithoutPhases`
helper: it keyed suppression/synthesis by measurand only, so a single
aggregate `Energy.Active.Import.Register` template anywhere in the
bucket satisfied `hasAggregate` and suppressed per-phase L-N
templates in every other identity family. Example: connector
configured with per-phase INLET templates plus an aggregate OUTLET
template would drop the INLET family entirely and synthesize nothing
for it, violating the spec requirement to report the total energy
per configured family.

Redesign the helper to partition templates into identity families
keyed by `(context, format, location, unit)` before applying
suppression/synthesis:

- Per-phase L-N templates in each family are filtered out (no
  behavioral change per family).
- Within each family that had per-phase L-N templates but no
  aggregate, an aggregate is synthesized from the first suppressed
  L-N template of that family (`phase` cleared, other identity
  fields inherited via shallow spread).
- Families that had no per-phase L-N templates are untouched.
- Result is re-sorted by `PHASE_RANK` to preserve stable emit order.

Extract the `phaseFamily(t.phase) === 'LineToNeutral'` predicate
into `isLineToNeutralTemplate` per R8 Lane A NIT (DRY, drops
redundant `t.phase != null` guard because `phaseFamily(undefined)`
is always `'Aggregate'`, never `'LineToNeutral'`).

Normalize terminology `L-N per-phase` -> `per-phase L-N` in JSDoc
and wire comments per R8 Lane C NIT (single term per concept).

One regression test added exercising the mixed-family case:
INLET per-phase L-N templates plus an OUTLET aggregate template
with the flag enabled must emit two aggregate samples (INLET
synthesized, OUTLET preserved), not one.

Test invariant `fail: 0, skipped: 6` preserved.

Spec citation: OCPP 2.0.1 DMD
`docs/ocpp2/Appendices_CSV_v1.4/dm_components_vars.csv:213`:
`SampledDataCtrlr;RegisterValuesWithoutPhases;;no;boolean` -
"will only report the total energy over all phases".

* refactor(meter-values): use Map.groupBy in RegisterValuesWithoutPhases helper (issue #1936 k)

Redesign `applyRegisterValuesWithoutPhases` per session-established
criteria for elegance, harmonization, and TS/JS state-of-the-art:

- Replace the imperative two-Maps-plus-double-loop dance
  (`perPhaseLNByFamily` + `otherByFamily`) with a single
  `Map.groupBy(bucket, templateFamilyKey)` call, harmonizing with the
  sibling `groupTemplatesByMeasurand` helper in the same file. Both
  now use the same ES2024 grouping primitive for the same conceptual
  operation.
- Extract `templateFamilyKey` to module scope alongside the sibling
  `phaseFamily` and `isLineToNeutralTemplate` helpers, matching the
  file's helper-placement convention (private module-scope pure
  functions above the exported builder).
- Inline the synthesis into a single `surviving.push(...)` call
  (removes the intermediate `synthesized` local).
- Net -4 lines with clearer control flow: per-family filter for
  L-N vs non-L-N, three explicit branches (no L-N to suppress,
  aggregate already configured, synthesize from first L-N),
  followed by a single stable sort.

No behavioral change: the mixed-family regression test at
`CoherentMeterValues.test.ts:1256+` (INLET per-phase L-N +
OUTLET aggregate) still asserts both families emit an aggregate
(INLET synthesized, OUTLET preserved). All prior R1-R8 tests
pass unchanged.

Test invariant `fail: 0, skipped: 6` preserved.

* docs(meter-values): describe per-family synthesis in README and JSDoc (issue #1936 k)

R10 Lane B cross-artifact alignment audit caught 2 MINOR wording drifts
carried over from the pre-R8 (pre-family-partitioning) design that R9
Lane C had implicitly accepted as converged:

1. `README.md:496` Energy.Active.Import.Register bullet described
   synthesis as connector-global ("if no aggregate register template
   is configured, one is synthesized...") without the per-family
   qualifier. HEAD's `applyRegisterValuesWithoutPhases` groups
   templates into identity families keyed by `(context, format,
   location, unit)` and synthesizes an aggregate per family that
   lacks one, so the wording was implying a single global aggregate
   where the code emits one per family.

2. `CoherentMeterValueBuilder.ts` `buildCoherentMeterValue` JSDoc
   `@param registerValuesWithoutPhases` used the same pre-family
   phrasing ("if the connector configures only per-phase L-N
   templates (no aggregate), an aggregate template is synthesized").
   The helper JSDoc was already accurate; only the exported API
   documentation lagged the redesign.

Both descriptions rewritten to state the identity-family key
explicitly and the per-family synthesis semantic. No code change.

Test invariant `fail: 0, skipped: 6` preserved.
README.md
src/charging-station/meter-values/CoherentMeterValueBuilder.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
src/types/ocpp/2.0/Variables.ts
tests/charging-station/meter-values/CoherentMeterValues.test.ts
tests/charging-station/meter-values/StrategyDispatch.test.ts