Flow: virtual "sum" meters that aggregate upstreams + demo chain
ci / build-test (push) Successful in 1m14s
ci / build-test (push) Successful in 1m14s
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
This commit is contained in:
@@ -59,6 +59,18 @@ public sealed class FlowService(IDbContextFactory<MeterVaultDbContext> contextFa
|
||||
|
||||
var depth = ComputeDepths(meters.Select(m => m.Id).ToList(), parents, children);
|
||||
|
||||
// Aggregate ("sum") meters: a Virtual-mode meter has no measurements of its own — in the flow
|
||||
// it is the sum of its upstream meters (e.g. "Sum Solar" = Solar 1 + Solar 2). Resolve these in
|
||||
// topological (depth) order so each aggregate sees its already-resolved upstream values.
|
||||
var aggregates = meters.Where(m => m.Mode == MeterMode.Virtual).Select(m => m.Id).ToHashSet();
|
||||
foreach (var id in meters.Select(m => m.Id).OrderBy(id => depth.GetValueOrDefault(id)))
|
||||
{
|
||||
if (aggregates.Contains(id))
|
||||
{
|
||||
value[id] = parents[id].Sum(V);
|
||||
}
|
||||
}
|
||||
|
||||
// Link value: a child's consumption flows in from its parent(s); with several parents it is
|
||||
// split proportionally to the parents' own consumption (equal split if those are all zero).
|
||||
var flowLinks = new List<FlowLink>();
|
||||
|
||||
@@ -51,13 +51,15 @@ public sealed class ReferenceDataImporter(MeterVaultDbContext db, ImportService
|
||||
var wasser = Meter("Zähler Wasser", water, MeterMode.CumulativeCounter, "m3", initialBaseline: 820);
|
||||
var oilTank = Meter("Öltank", oil, MeterMode.ConsumableBalance, "L");
|
||||
var burner = Meter("Brenner", oil, MeterMode.RuntimeCounter, "h");
|
||||
// A virtual "sum" meter: no readings of its own — in the flow view it equals Solar 1 + Solar 2.
|
||||
var sumSolar = Meter("Summe Solar", electricity, MeterMode.Virtual, "kWh");
|
||||
|
||||
// Tag the PV meters' roles (config, not hardcoded names) so the Solar panel can derive
|
||||
// self-consumption = total_load − grid_import and savings generically (SDD §8.4).
|
||||
haus.Meta = MeterMeta.WithRole(haus.Meta, MeterRoles.TotalLoad);
|
||||
netz.Meta = MeterMeta.WithRole(netz.Meta, MeterRoles.GridImport);
|
||||
|
||||
_db.Meters.AddRange(haus, netz, auto, solar1, solar2, wasser, oilTank, burner);
|
||||
_db.Meters.AddRange(haus, netz, auto, solar1, solar2, wasser, oilTank, burner, sumSolar);
|
||||
await _db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_db.Tanks.Add(new Tank
|
||||
@@ -68,9 +70,15 @@ public sealed class ReferenceDataImporter(MeterVaultDbContext db, ImportService
|
||||
Calibration = $"{{\"volumePerUnit\":{ReferenceProfiles.OilLitresPerCm.ToString(System.Globalization.CultureInfo.InvariantCulture)}}}",
|
||||
});
|
||||
|
||||
// Demo flow chain: car charging is a subsection of total house load (Haus → Auto), so the
|
||||
// electricity flow view shows Haus dividing into Auto + an "Other" remainder.
|
||||
_db.MeterLinks.Add(new MeterLink { FromMeterId = haus.Id, ToMeterId = auto.Id });
|
||||
// Demo flow chain (electricity /energy view): Solar 1 + Solar 2 → Summe Solar; then
|
||||
// Grid + Summe Solar → Haus → Auto + "Other". The remainder under Grid/Summe Solar is the
|
||||
// input that didn't reach the house load (solar export / battery / inverter losses).
|
||||
_db.MeterLinks.AddRange(
|
||||
new MeterLink { FromMeterId = solar1.Id, ToMeterId = sumSolar.Id },
|
||||
new MeterLink { FromMeterId = solar2.Id, ToMeterId = sumSolar.Id },
|
||||
new MeterLink { FromMeterId = netz.Id, ToMeterId = haus.Id },
|
||||
new MeterLink { FromMeterId = sumSolar.Id, ToMeterId = haus.Id },
|
||||
new MeterLink { FromMeterId = haus.Id, ToMeterId = auto.Id });
|
||||
|
||||
AddElectricityTariffs(electricity);
|
||||
AddTariff(TariffScope.EnergyType, water, 5.00, "EUR/m3", new DateOnly(2022, 11, 1));
|
||||
|
||||
Reference in New Issue
Block a user