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:
@@ -0,0 +1,90 @@
|
||||
using MeterVault.Core.Domain;
|
||||
using MeterVault.Core.Normalization;
|
||||
using static MeterVault.Core.Tests.TestData;
|
||||
|
||||
namespace MeterVault.Core.Tests;
|
||||
|
||||
public sealed class RuntimeAndTankTests
|
||||
{
|
||||
private readonly INormalizationEngine _engine = NormalizationEngine.CreateDefault();
|
||||
|
||||
[Fact]
|
||||
public void Runtime_counter_multiplies_delta_hours_by_fixed_rate()
|
||||
{
|
||||
var ctx = new NormalizationContext
|
||||
{
|
||||
Meter = new MeterConfig
|
||||
{
|
||||
MeterId = 20,
|
||||
Mode = MeterMode.RuntimeCounter,
|
||||
Unit = "L",
|
||||
InitialBaseline = 7785,
|
||||
Tank = new TankConfig { Capacity = 7000, RateMode = TankRateMode.Fixed, FixedRate = 2.0 },
|
||||
},
|
||||
Readings = [Reading(20, Month(2023, 1), 7785), Reading(20, Month(2023, 2), 7952)],
|
||||
};
|
||||
|
||||
var result = _engine.Normalize(ctx);
|
||||
|
||||
// Δhours: 0, then 167 × 2.0 L/h = 334 L.
|
||||
Assert.Equal([0d, 334d], result.Select(c => c.Amount));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tank_consumption_is_level_delta_between_dipsticks()
|
||||
{
|
||||
// Linear calibration 7000 L / 150 cm ≈ 46.667 L/cm (SDD §2.4).
|
||||
var calibration = new CalibrationCurve(7000d / 150d);
|
||||
var ctx = new NormalizationContext
|
||||
{
|
||||
Meter = new MeterConfig
|
||||
{
|
||||
MeterId = 30,
|
||||
Mode = MeterMode.ConsumableBalance,
|
||||
Unit = "L",
|
||||
Tank = new TankConfig { Capacity = 7000, Calibration = calibration },
|
||||
},
|
||||
Events =
|
||||
[
|
||||
Delivery(30, Month(2020, 9), 3500), // pre-first-level delivery: absorbed
|
||||
TankLevelCm(30, Month(2022, 9), 35), // first level: no consumption emitted
|
||||
TankLevelCm(30, Month(2022, 10), 34), // ~46.7 L drawn
|
||||
TankLevelCm(30, Month(2022, 11), 27), // ~326.7 L drawn
|
||||
],
|
||||
};
|
||||
|
||||
var result = _engine.Normalize(ctx);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal(46.7, result[0].Amount, 1);
|
||||
Assert.Equal(326.7, result[1].Amount, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delivery_between_dipsticks_reconciles_into_the_balance()
|
||||
{
|
||||
var calibration = new CalibrationCurve(7000d / 150d);
|
||||
var ctx = new NormalizationContext
|
||||
{
|
||||
Meter = new MeterConfig
|
||||
{
|
||||
MeterId = 30,
|
||||
Mode = MeterMode.ConsumableBalance,
|
||||
Unit = "L",
|
||||
Tank = new TankConfig { Capacity = 7000, Calibration = calibration },
|
||||
},
|
||||
Events =
|
||||
[
|
||||
TankLevelCm(30, Month(2022, 9), 20), // 933.3 L
|
||||
Delivery(30, Month(2022, 10), 2000),
|
||||
TankLevelCm(30, Month(2022, 11), 50), // 2333.3 L
|
||||
],
|
||||
};
|
||||
|
||||
var result = _engine.Normalize(ctx);
|
||||
|
||||
// consumption = 933.3 + 2000 − 2333.3 = 600.
|
||||
Assert.Single(result);
|
||||
Assert.Equal(600d, result[0].Amount, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user