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.