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