Adds the "sum meter" the user asked for: a meter that doesn't physically exist
but represents the sum of other meters in the flow view.
- FlowService: a Virtual-mode meter has no readings of its own; its flow value is
the sum of its upstream meters, resolved in topological order (so "Summe Solar"
= Solar 1 + Solar 2, and Grid + Summe Solar → House with the remainder as
"Other" = export / battery / inverter losses).
- Meters editor: a hint when Mode = Virtual explaining the sum-meter behaviour.
- Reference data seeds the full demo chain: Solar 1 + Solar 2 → Summe Solar;
Netz + Summe Solar → Haus → Auto + Other — so /energy/1 shows a multi-level
Sankey out of the box.
- Fix: SankeyChart Unit was passed as the literal string "_graph.Unit" (missing
@) so node labels read "_graph.Unit" instead of "kWh". Caught by screenshotting
the live page.
Test: Virtual_sum_meter_aggregates_its_upstreams. 69 Core + 50 Integration =
119 green. Live-verified with a browser screenshot of the multi-level flow.
Claude-Session: https://claude.ai/code/session_01Lz2RqAsnQhetqWNoCDfexK
A meter's flow value is now its throughput — consumption OR generation output —
so a generation meter (solar) acts as a source that can feed downstream meters.
Setting a load meter's upstream to {grid, solar} now splits its consumption
across both proportionally, and the remainder under the sources (grid + solar −
load) surfaces as "Other" = export + battery/inverter losses. Negative values
(savings/balance virtuals) are clamped to 0 (a ribbon can't be negative). The
per-type KPI is relabelled "Top-level throughput" since it now spans generation.
Test: Generation_meter_counts_as_source (grid 75 + solar-gen 30 → house 40 →
28.57/11.43 split, 65 remainder). 69 Core + 48 Integration = 117 green.
Live-verified: electricity flow now shows Solar 1/2 as source nodes.
Claude-Session: https://claude.ai/code/session_01Lz2RqAsnQhetqWNoCDfexK
Adds a meter hierarchy and a flow view: a downstream meter is a *subsection*
of an upstream one (not an addition), so you can see where a main meter's flow
divides — e.g. official water → garden, pool, other; grid/battery → all → car.
- MeterLink (schema + migration AddMeterLinks): a directed from→to flow edge.
Multi-parent allowed (a merge, e.g. grid + solar → house); multi-child is a
split. Cascade-deletes with either endpoint; unique + distinct-endpoint checks.
- FlowService: per energy type + period, builds a Sankey graph — nodes = meters
sized by consumption; link value = downstream meter's consumption, split
proportionally across multiple upstreams; unaccounted remainder under a meter
becomes a synthetic "Other" node; depth via topological longest-path.
- SankeyChart.razor: hand-rolled inline-SVG Sankey (ApexCharts has no Sankey
type) — columns by depth, nodes stacked by value, bezier ribbons sized by flow,
left→right, theme-aware, HTML-encoded labels, tooltips. Built as a MarkupString
to sidestep Razor's <text> element clash.
- /energy/{id} page (one per energy type): KPIs (consumption + cost), the flow
Sankey, and the meter list. NavMenu now lists a link per energy type
(Electricity, Water, Gas, …) loaded from the DB.
- Meters admin: cycle-safe "Sub-meter of (upstream meters)" multi-select
(descendants excluded to prevent cycles); reconciles meter_link rows on save.
- Reference data seeds a demo chain (Haus → Auto) so electricity flow shows
Haus dividing into Auto + Other.
Tests: FlowServiceTests (single-parent remainder; two-parent proportional
split); render test now asserts the flow chain + covers /energy/{id}. 69 Core +
47 Integration = 116 green. Live-verified: Haus 95,450 kWh → Auto 51,909 +
Other 43,541 (flow conserved), all 5 energy-type pages render.
Claude-Session: https://claude.ai/code/session_01Lz2RqAsnQhetqWNoCDfexK