]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(meter-values): evse-level MeterValues template fallback in coherent path (issue...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 5 Jul 2026 00:06:37 +0000 (02:06 +0200)
committerGitHub <noreply@github.com>
Sun, 5 Jul 2026 00:06:37 +0000 (02:06 +0200)
commita0cdcfc95958293619e3d7a4028f2e05b0cd6cca
tree0e5d45409602b8d051123aea66869d8b91b967fc
parent2cbd92dd0ada34cc10982629fae91cac2e165593
feat(meter-values): evse-level MeterValues template fallback in coherent path (issue #1936 g) (#1944)

* feat(meter-values): evse-level MeterValues template fallback in coherent path (issue #1936 g)

The coherent path's `resolveTemplates` at `CoherentMeterValueBuilder.ts`
read connector-level `MeterValues` only. A station that defined
`MeterValues` at EVSE level (via the `Evses` template section) and
enabled `coherentMeterValues=true` would emit an empty MeterValue
because the EVSE-level array was never consulted — a pre-existing TODO
documented in the prior `resolveTemplates` JSDoc: "Adding EVSE-level
template inheritance to the coherent path requires extending the
context interface with `getEvseStatus`."

Fix: mirror the random/fixed path's `getSampledValueTemplate` semantics
(README section "MeterValues at EVSE level": "EVSE-level definitions
apply to all connectors of the EVSE and override connector-level
definitions"). EVSE-level `MeterValues`, when defined and non-empty,
override connector-level `MeterValues` for every connector under the
EVSE; connector-level `MeterValues` are used when the connector is not
grouped under an EVSE (flat `Connectors` map station layout) or when
the EVSE-level array is empty.

- Extend `ICoherentContext` with two accessors mirroring the existing
  `ChargingStation` public API: `getEvseIdByConnectorId(connectorId)`
  and `getEvseStatus(evseId)`. Requires importing `EvseStatus` from
  the shared types barrel.
- Rewrite `resolveTemplates` in `CoherentMeterValueBuilder.ts` to
  perform the EVSE-first lookup and fall back to connector-level when
  the EVSE is undefined or its `MeterValues` array is missing/empty.
  JSDoc updated to reflect the new contract.
- Extend the in-file `buildContext` test factory with an optional
  `evseMeterValues` override (implies `getEvseIdByConnectorId`
  resolves to EVSE id 1 and `getEvseStatus(1)` returns
  `{ MeterValues: overrides.evseMeterValues, ... }`; falsy override
  leaves `getEvseIdByConnectorId` returning `undefined`).
- Add an `EVSE-level template fallback` describe block with two
  tests: (a) EVSE-level MeterValues override connector-level when
  defined, (b) connector-level MeterValues used when no EVSE
  grouping exists.
- Update README section "Template resolution scope" to remove the
  "connector-level definition only" caveat and document the new
  precedence rules.

The shared mock in `tests/charging-station/helpers/StationHelpers.ts`
already exposes both accessors; downstream test files routing through
the mock require no change.

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

Closes issue #1936 item (g).

* docs(meter-values): clarify EVSE fallback semantics + add empty-EVSE test (issue #1936 g)

R1 Lane A (Oracle) surfaced two follow-ups on the initial R1 rebase
implementation of the EVSE-first template lookup:

1. The `resolveTemplates` JSDoc and README wording claimed "the same
   precedence as ... `getSampledValueTemplate`". That claim was
   inaccurate on a subtle edge case: the random/fixed reference at
   `OCPPServiceUtils.ts:1546-1563` aggregates `MeterValues` across all
   sibling connectors under the same EVSE when `EvseStatus.MeterValues`
   is empty. The coherent path deliberately does NOT do this - a
   connector without its own `MeterValues` returns `undefined` under
   the coherent path, whereas the reference would leak sibling
   templates. This stricter isolation is desirable design (per-connector
   ownership), but the parity claim overstated the match.

   Wording weakened accordingly: JSDoc + README now explicitly state
   that the coherent generator emits templates from exactly one source
   (EVSE-level or the queried connector) and does not aggregate across
   sibling connectors, calling out the divergence from the reference
   path.

2. The initial R1 rebase covered "EVSE-level overrides connector-level
   when defined" and "connector-level used when no EVSE grouping" but
   left the third semantic branch untested: EVSE grouping present +
   `EvseStatus.MeterValues` is an empty array. The `resolveTemplates`
   guard `evseTemplates != null && evseTemplates.length > 0` explicitly
   handles this case by falling back to connector-level, but no test
   asserted the branch.

   New test added: `should fall back to connector-level MeterValues
   when EVSE grouping exists but EVSE-level MeterValues array is
   empty`. Configures `evseMeterValues: []` in `buildContext` (which
   makes `getEvseIdByConnectorId` return `1` and
   `getEvseStatus(1).MeterValues = []`), asserts connector-level
   template emits.

No code change to `resolveTemplates`; the empty-EVSE branch was
already correct.

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

* test(meter-values): cover EVSE-grouped + undefined MeterValues fallback (issue #1936 g)

R2 Lane B (adversarial) noted that the previous 3-test coverage in the
`EVSE-level template fallback` describe block conflated two distinct
cases: "no EVSE grouping" and "EVSE grouping present with
`MeterValues` undefined". Both eventually fall back to connector-level,
but through different code paths in `resolveTemplates`:

- "No EVSE grouping": `getEvseIdByConnectorId` returns `undefined` -
  the outer `if (evseId != null)` guard fails, fallback immediate.
- "EVSE grouping + MeterValues undefined": `getEvseIdByConnectorId`
  returns `1`, `getEvseStatus(1).MeterValues` is `undefined`, so the
  inner `evseTemplates != null && evseTemplates.length > 0` guard
  fails (both undefined and empty-array branches share the fallback).

The prior mock factory heuristic `overrides.evseMeterValues != null ?
1 : undefined` could express the first two cases plus "grouping +
empty array" but NOT "grouping + undefined MeterValues" - those
required distinct mock states.

Redesigned factory:
- Add an explicit `groupUnderEvse?: boolean` knob that overrides the
  `evseMeterValues != null` heuristic when set.
- `groupUnderEvse === true` forces `getEvseIdByConnectorId` to return
  `1` regardless of `evseMeterValues`, enabling the "grouping +
  undefined MeterValues" state via `groupUnderEvse: true` alone.
- `groupUnderEvse === false` forces flat-connector layout even with
  `evseMeterValues` defined (fully explicit override).
- `groupUnderEvse === undefined` preserves the prior heuristic
  (backward-compat for existing tests).

New test added: `should fall back to connector-level MeterValues when
EVSE grouping exists but EVSE-level MeterValues is undefined`. Uses
`groupUnderEvse: true` without `evseMeterValues` to exercise the
distinct-from-empty-array branch.

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

* docs(meter-values): sharpen resolveTemplates JSDoc + adopt isNotEmptyArray helper (issue #1936 g)

R8 (comprehensive final review, user-directed expanded criteria) surfaced
2 tightly related follow-ups on the R1-established `resolveTemplates`
implementation:

1. Lane C MINOR (JSDoc precision drift): the divergence NOTE narrowed
   the condition under which the random/fixed path aggregates
   sibling-connector templates to "both EVSE-level and the queried
   connector's `MeterValues` are empty". The reference
   `getSampledValueTemplate` (`OCPPServiceUtils.ts:1546-1559`) actually
   aggregates whenever `isNotEmptyArray(evseStatus.MeterValues)` is
   false, which covers `undefined` and `[]` regardless of the queried
   connector's state. The NOTE was strictly narrower than reality; the
   README and PR body already used the broader wording, leaving only
   the JSDoc drifted.

   NOTE rewritten: "when EVSE-level `MeterValues` is undefined or
   empty" (matches reference guard exactly), with the source clause
   correspondingly rephrased "EVSE-level when non-empty, otherwise the
   queried connector".

2. Lane A NIT (harmonization with cross-linked sibling): `resolveTemplates`
   used inline `evseTemplates != null && evseTemplates.length > 0`; the
   explicitly cross-linked sibling `getSampledValueTemplate` uses
   `isNotEmptyArray(evseStatus.MeterValues)` for the identical guard.
   `isNotEmptyArray` is a repo-wide type guard (`Utils.ts:411`,
   `value is NonEmptyArray<T> | ReadonlyNonEmptyArray<T>`) that also
   narrows the return type. Behavioral equivalence, better locality
   with sibling code, one less inline predicate.

   Guard rewritten to `if (isNotEmptyArray(evseTemplates))`; the utils
   barrel import now includes `isNotEmptyArray`.

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

* test(meter-values): lock non-aggregation divergence + terminology hyphenation (issue #1936 g)

R9 (post-R8-fix comprehensive re-review, user-directed expanded criteria)
surfaced 3 follow-ups on the R8-committed branch state:

1. Lane B MINOR (regression test gap): the documented divergence from
   `getSampledValueTemplate` (coherent path does NOT aggregate
   sibling-connector `MeterValues` when EVSE-level is undefined or
   empty) was described in JSDoc, README, and PR body but not
   contractually locked by a test. The prior test harness had a
   single-connector EVSE (`connectors: Map([[1, connectorStatus]])`),
   which precluded exercising the sibling-aggregation branch.

   `buildContext` extended with `siblingConnectorMeterValues?:
   SampledValueTemplate[]` knob: when defined, the mock's
   `getEvseStatus(1).connectors` gains a second connector (id 2) with
   the given `MeterValues`. New test
   `should NOT aggregate sibling-connector MeterValues when EVSE-level
   MeterValues is undefined or empty` asserts the coherent path emits
   zero samples when queried connector's `MeterValues` is `[]`,
   EVSE-level `MeterValues` is `[]`, and a sibling connector carries a
   Power template. The reference path would emit that Power template;
   the coherent path must not.

2. Lane B NIT (stale test comment): the comment at
   `CoherentMeterValues.test.ts:1500-1503` still named the pre-R8
   inline guard `evseTemplates != null && length > 0`. Updated to
   reference `isNotEmptyArray(evseTemplates)` matching the current
   implementation at `CoherentMeterValueBuilder.ts:335-337`.

3. Lane B SUB-THRESHOLD (README hyphenation): `README.md:367,369,409`
   used `EVSE level` / `connector level` unhyphenated, drifting from
   the hyphenated `EVSE-level` / `connector-level` used at
   `README.md:489` and throughout the JSDoc/PR body/tests. Normalized
   the 3 remaining occurrences to the hyphenated form for repo-wide
   terminology consistency.

Local report `.omo/reviews/PR-1944.md` also refreshed post-R8 append
drift (Convergence Summary claimed "7 rounds" when 8 were now
documented; R8 consolidation arithmetic `12+ + 6 + 9 = 27+` was
written as `21+`).

Test invariant `fail: 0, skipped: 6` preserved (37/37 pass in target
file including the new sibling-aggregation regression test).
README.md
src/charging-station/meter-values/CoherentMeterValueBuilder.ts
src/charging-station/meter-values/types.ts
tests/charging-station/meter-values/CoherentMeterValues.test.ts