Meter chain topology + per-energy-type flow (Sankey) pages
ci / build-test (push) Successful in 1m24s
ci / build-test (push) Successful in 1m24s
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
This commit is contained in:
@@ -46,6 +46,7 @@ public sealed class DashboardRenderTests(TimescaleFixture fx)
|
||||
|
||||
// Panel read models compute real figures from the reference data (SDD §8.4–§8.6).
|
||||
int hausId;
|
||||
short electricityTypeId;
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
@@ -72,6 +73,13 @@ public sealed class DashboardRenderTests(TimescaleFixture fx)
|
||||
Assert.NotNull(detail);
|
||||
Assert.True(detail!.ReadingCount > 0);
|
||||
Assert.True(detail.TotalConsumption > 0);
|
||||
|
||||
// Flow graph: the demo Haus → Auto chain yields a link + an "Other (Haus)" remainder.
|
||||
electricityTypeId = await db.EnergyTypes.Where(t => t.Key == "electricity").Select(t => t.Id).FirstAsync();
|
||||
var flow = await services.GetRequiredService<FlowService>()
|
||||
.GetFlowAsync(electricityTypeId, new DateOnly(1997, 1, 1), new DateOnly(2027, 1, 1));
|
||||
Assert.True(flow.HasChain);
|
||||
Assert.Contains(flow.Nodes, n => n.IsOther);
|
||||
}
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
@@ -91,6 +99,7 @@ public sealed class DashboardRenderTests(TimescaleFixture fx)
|
||||
"/meters", "/trends", "/solar", "/consumables", "/import",
|
||||
"/admin/tariffs", "/admin/energy-types", "/admin/categories",
|
||||
"/admin/connectors", "/admin/settings", $"/meters/{hausId}",
|
||||
$"/energy/{electricityTypeId}",
|
||||
})
|
||||
{
|
||||
var response = await client.GetAsync(new Uri(path, UriKind.Relative));
|
||||
@@ -106,6 +115,7 @@ public sealed class DashboardRenderTests(TimescaleFixture fx)
|
||||
|
||||
private static async Task ClearDataAsync(MeterVaultDbContext db)
|
||||
{
|
||||
await db.MeterLinks.ExecuteDeleteAsync();
|
||||
await db.Consumption.ExecuteDeleteAsync();
|
||||
await db.Readings.ExecuteDeleteAsync();
|
||||
await db.MeterEvents.ExecuteDeleteAsync();
|
||||
|
||||
Reference in New Issue
Block a user