using MeterVault.Core.Domain; using MeterVault.Core.Normalization; using static MeterVault.Core.Tests.TestData; namespace MeterVault.Core.Tests; /// /// instant_rate: the reading value is an instantaneous rate (e.g. kW) integrated over time into /// consumption (kWh). Uses the trapezoidal rule between consecutive samples, attributed to the /// interval's end reading — the first reading only seeds the integral. /// public sealed class InstantRateNormalizerTests { private readonly INormalizationEngine _engine = NormalizationEngine.CreateDefault(); private static DateTimeOffset At(int hour) => new(new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc).AddHours(hour)); [Fact] public void Constant_rate_integrates_to_rate_times_hours() { // 2 kW held steady for 3 hours → 6 kWh, booked at the end of the interval. var ctx = new NormalizationContext { Meter = new MeterConfig { MeterId = 1, Mode = MeterMode.InstantRate, Unit = "kWh" }, Readings = [Reading(1, At(0), 2), Reading(1, At(3), 2)], }; var result = _engine.Normalize(ctx); var row = Assert.Single(result); Assert.Equal(6d, row.Amount, 6); Assert.Equal(At(3), row.Time); Assert.Equal(ConsumptionKind.Consumption, row.Kind); } [Fact] public void Linear_ramp_integrates_trapezoidally_per_interval() { // 0 kW → 2 kW → 4 kW at 1-hour steps. Intervals: (0+2)/2·1 = 1, (2+4)/2·1 = 3. var ctx = new NormalizationContext { Meter = new MeterConfig { MeterId = 1, Mode = MeterMode.InstantRate, Unit = "kWh" }, Readings = [Reading(1, At(0), 0), Reading(1, At(1), 2), Reading(1, At(2), 4)], }; var result = _engine.Normalize(ctx); // First reading only seeds the integral: N readings → N−1 rows. Assert.Equal([1d, 3d], result.Select(c => c.Amount)); Assert.Equal([At(1), At(2)], result.Select(c => c.Time)); } [Fact] public void Negative_rate_is_preserved_as_export() { // A bidirectional power sensor reading −4 kW for an hour → −4 kWh (net export), not clamped. var ctx = new NormalizationContext { Meter = new MeterConfig { MeterId = 1, Mode = MeterMode.InstantRate, Unit = "kWh" }, Readings = [Reading(1, At(0), -4), Reading(1, At(1), -4)], }; var result = _engine.Normalize(ctx); Assert.Equal(-4d, Assert.Single(result).Amount, 6); } [Fact] public void Single_reading_produces_no_consumption() { var ctx = new NormalizationContext { Meter = new MeterConfig { MeterId = 1, Mode = MeterMode.InstantRate, Unit = "kWh" }, Readings = [Reading(1, At(0), 5)], }; Assert.Empty(_engine.Normalize(ctx)); } }