M1: pure normalization engine + default seed

Infrastructure-free normalization engine in Core dispatching on MeterMode:
- Cumulative/Generation counters: register deltas, first-reading baseline (Haus 411,
  Auto 3755), meter swaps (water …861→2 reconciles to 12 via boundary registers OR an
  explicit amount override), counter resets, anomaly-guarded decreases.
- RuntimeCounter: Δhours × rate (fixed/empirical).
- ConsumableBalance: tank level-Δ + deliveries → consumption, cm→litre calibration;
  delivery-only rows before the first dipstick emit nothing.
- DirectDelta, and Virtual meters via a small safe arithmetic evaluator (Netz Einsparung
  = Haus − Netz, Eigenverbrauch = Erzeugung − Einsparung) — data-driven, not hardcoded.
- DatabaseSeeder: default energy types, cost categories, base settings (idempotent).

24 Core unit tests + 4 integration tests green.

Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
This commit is contained in:
2026-07-13 11:12:03 +02:00
parent 48a7f5a825
commit d972f67bad
21 changed files with 1121 additions and 1 deletions
+75
View File
@@ -0,0 +1,75 @@
using MeterVault.Core.Domain;
using MeterVault.Core.Normalization;
using static MeterVault.Core.Tests.TestData;
namespace MeterVault.Core.Tests;
/// <summary>
/// The electricity derived columns are data-driven virtual expressions, not hardcoded formulas
/// (SDD §2.2, §7.4): Netz Einsparung = Haus Netz, Anlage Eigenverbrauch = Erzeugung Einsparung.
/// </summary>
public sealed class VirtualMeterTests
{
private readonly INormalizationEngine _engine = NormalizationEngine.CreateDefault();
private static Consumption Cons(int meterId, DateTimeOffset time, double amount) => new()
{
MeterId = meterId,
Time = time,
Amount = amount,
Kind = ConsumptionKind.Consumption,
Quality = ReadingQuality.Imported,
};
[Fact]
public void Netz_einsparung_is_haus_minus_netz()
{
var oct = Month(2022, 10);
var nov = Month(2022, 11);
var ctx = new NormalizationContext
{
Meter = new MeterConfig
{
MeterId = 100,
Mode = MeterMode.Virtual,
Unit = "kWh",
Virtual = new VirtualSpec { Expression = "m1 - m2", ReferencedMeterIds = [1, 2] },
},
ReferencedSeries = new Dictionary<int, IReadOnlyList<Consumption>>
{
[1] = [Cons(1, oct, 411), Cons(1, nov, 742)], // Haus Verbrauch
[2] = [Cons(2, oct, 416), Cons(2, nov, 832)], // Netz Verbrauch
},
};
var result = _engine.Normalize(ctx);
Assert.Equal([-5d, -90d], result.Select(c => c.Amount));
}
[Fact]
public void Eigenverbrauch_is_erzeugung_minus_einsparung()
{
var oct = Month(2022, 10);
var ctx = new NormalizationContext
{
Meter = new MeterConfig
{
MeterId = 101,
Mode = MeterMode.Virtual,
Unit = "kWh",
Virtual = new VirtualSpec { Expression = "m50 - m100", ReferencedMeterIds = [50, 100] },
},
ReferencedSeries = new Dictionary<int, IReadOnlyList<Consumption>>
{
[50] = [Cons(50, oct, 76)], // Solar Erzeugung
[100] = [Cons(100, oct, -5)], // Netz Einsparung (from the previous test)
},
};
var result = _engine.Normalize(ctx);
// 76 (5) = 81 (Anlage Eigenverbrauch Okt 2022).
Assert.Equal([81d], result.Select(c => c.Amount));
}
}