using MeterVault.Core.Analysis;
using MeterVault.Core.Analysis.Coverage;
using MeterVault.Core.Domain;
using MeterVault.Core.Normalization;
using MeterVault.Infrastructure.Import;
using static MeterVault.Integration.Tests.Reconciliation.ReconciliationSupport;
namespace MeterVault.Integration.Tests.Reconciliation;
///
/// Coverage runs (D-13) of the seeded reference sheets, in the instance timezone. These are the shapes
/// the analysis pages meet first: monthly sheets that resolve months exactly, a burner read once in twelve
/// years before its monthly rows begin, and a tank whose deliveries go back to 1997 but whose draw is only
/// known from the first dipstick in 2022.
///
public sealed class CoverageOfFixturesTests
{
private static readonly TimeZoneInfo Berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
private static IReadOnlyList Coverage(StagedImport staged, MeterConfig config) =>
CoverageBuilder.Build(
NormalizationEngine.CreateDefault().Normalize(new NormalizationContext
{
Meter = config,
Readings = staged.Readings.Where(r => r.MeterId == config.MeterId).ToList(),
Events = staged.Events.Where(e => e.MeterId == config.MeterId).ToList(),
TimeZone = Berlin,
}),
Berlin);
[Theory]
[InlineData(ReferenceProfiles.Haus, MeterMode.CumulativeCounter)]
[InlineData(ReferenceProfiles.Netz, MeterMode.CumulativeCounter)]
[InlineData(ReferenceProfiles.Auto, MeterMode.CumulativeCounter)]
[InlineData(ReferenceProfiles.Solar1, MeterMode.GenerationCounter)]
[InlineData(ReferenceProfiles.Solar2, MeterMode.GenerationCounter)]
public void Every_electricity_meter_is_one_month_run_divided_at_months(int meterId, MeterMode mode)
{
var staged = Stage(ReferenceProfiles.Electricity(), Electricity);
var readings = staged.Readings.Where(r => r.MeterId == meterId).OrderBy(r => r.Time).ToList();
var run = Assert.Single(Coverage(staged, new MeterConfig { MeterId = meterId, Mode = mode, Unit = "kWh" }));
Assert.Equal(GapAttribution.LabelMonthStart(readings[0], Berlin), run.From);
Assert.Equal(GapAttribution.EffectiveTime(readings[^1], Berlin), run.To);
Assert.Equal(GapAttribution.LabelMonthStart(readings[^1], Berlin), run.LastIntervalStart);
Assert.Equal(ResolutionClass.Month, run.Resolution);
Assert.True(run.DividedAtMonths);
Assert.False(run.IsGap);
}
[Fact]
public void Capping_the_electricity_sheet_inside_its_last_month_gives_up_exactly_that_month()
{
// A-04: the sheet's last row describes its whole month. A reader whose now falls in that month
// leaves the row out of actuals and its month uncovered, and keeps every month before it.
var staged = Stage(ReferenceProfiles.Electricity(), Electricity);
var readings = staged.Readings.Where(r => r.MeterId == ReferenceProfiles.Haus).OrderBy(r => r.Time).ToList();
var lastMonth = GapAttribution.LabelMonthStart(readings[^1], Berlin);
var stored = Assert.Single(Coverage(staged, new MeterConfig { MeterId = ReferenceProfiles.Haus, Mode = MeterMode.CumulativeCounter, Unit = "kWh" }));
var capped = Assert.Single(CoverageRuns.CapAt([stored], lastMonth.AddDays(10), Berlin));
Assert.Equal(stored.From, capped.From);
Assert.Equal(lastMonth, capped.To);
}
[Fact]
public void The_water_meter_is_covered_across_its_swap()
{
var staged = Stage(ReferenceProfiles.Water(), Water);
var run = Assert.Single(Coverage(staged, new MeterConfig
{
MeterId = ReferenceProfiles.Wasser, Mode = MeterMode.CumulativeCounter, Unit = "m3", InitialBaseline = 820,
}));
Assert.Equal(ResolutionClass.Month, run.Resolution);
Assert.True(run.DividedAtMonths);
}
[Fact]
public void The_seeded_burner_starts_with_its_twelve_year_interval_as_its_own_coarse_run()
{
var staged = Stage(ReferenceProfiles.HeatingOil(), Oil);
var readings = staged.Readings.Where(r => r.MeterId == ReferenceProfiles.Burner).OrderBy(r => r.Time).ToList();
var runs = Coverage(staged, new MeterConfig { MeterId = ReferenceProfiles.Burner, Mode = MeterMode.RuntimeCounter, Unit = "h" });
// 18.10.2010 → 14.10.2022: the first reading is an opening balance, the next closes twelve years.
Assert.Equal(
new CoverageRun(Utc(2010, 10, 18), Utc(2022, 10, 14), ResolutionClass.Coarse, DividedAtMonths: false, LastIntervalStart: Utc(2010, 10, 18)),
runs[0]);
Assert.Equal(readings[^1].Time, runs[^1].To);
Assert.Equal(ResolutionClass.Month, runs[^1].Resolution);
Assert.DoesNotContain(runs, r => r.IsGap);
for (var i = 1; i < runs.Count; i++)
{
Assert.Equal(runs[i - 1].To, runs[i].From);
}
}
[Fact]
public void The_seeded_tank_is_covered_from_its_first_dipstick_not_its_first_delivery()
{
var staged = Stage(ReferenceProfiles.HeatingOil(), Oil);
var levels = staged.Events
.Where(e => e.MeterId == ReferenceProfiles.OilTank && e.EventType == MeterEventType.TankLevel)
.OrderBy(e => e.Time)
.ToList();
Assert.Contains(staged.Events, e => e.MeterId == ReferenceProfiles.OilTank && e.EventType == MeterEventType.Delivery && e.Time.Year == 1997);
var runs = Coverage(staged, new MeterConfig
{
MeterId = ReferenceProfiles.OilTank,
Mode = MeterMode.ConsumableBalance,
Unit = "L",
Tank = new TankConfig { Capacity = 7000, Calibration = new CalibrationCurve(ReferenceProfiles.OilLitresPerCm) },
});
Assert.Equal(Utc(2022, 9, 8), runs[0].From);
Assert.Equal(levels[^1].Time, runs[^1].To);
Assert.DoesNotContain(runs, r => r.IsGap);
}
private static DateTimeOffset Utc(int year, int month, int day) => new(year, month, day, 0, 0, 0, TimeSpan.Zero);
}