M0: scaffold solution, EF+Timescale schema, /healthz

Five-project Clean Architecture solution (Core/Infrastructure/App + Core.Tests/
Integration.Tests) on .NET 10 with central package management, snake_case EF mapping,
and shared build/style config.

- Full domain entity set + EF DbContext for the SDD §5.3 schema (singular table names).
- InitialSchema migration (relational) + TimescaleHypertables migration (raw SQL:
  create_hypertable + compression on reading, hypertable on consumption).
- App wiring: Serilog (actually wired, unlike MQTTower), DbContext, migrate-on-startup,
  /healthz. Serves plain HTTP behind a reverse proxy (no HTTPS redirect).
- deploy/Dockerfile (2-stage, ICU-capable) + docker-compose (app + timescaledb).
- Integration.Tests: shared TimescaleFixture (Testcontainers) — migrations, hypertables,
  compression policy, and /healthz all verified green (4/4).

Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
This commit is contained in:
2026-07-13 11:02:21 +02:00
commit 48a7f5a825
65 changed files with 5896 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
namespace MeterVault.Core.Domain;
/// <summary>
/// How a meter's raw readings become normalized consumption (SDD §5.2).
/// Stored as text in the database via a value converter.
/// </summary>
public enum MeterMode
{
/// <summary>Monotonic register (electricity house/grid/EV, water). Consumption = Δ register, with swaps/resets.</summary>
CumulativeCounter,
/// <summary>Monotonic generation register (PV). Δ register → generation.</summary>
GenerationCounter,
/// <summary>Cumulative operating hours (burner). Δ hours × rate (fixed or empirical) → consumption.</summary>
RuntimeCounter,
/// <summary>Tank/bottle with deliveries + level. Deliveries add; usage from level-Δ and/or runtime×rate; forecast to empty.</summary>
ConsumableBalance,
/// <summary>Source already reports increments. The value is the increment.</summary>
DirectDelta,
/// <summary>Power/flow sensor (schema-supported; worker deferred post-v1). Integrate rate over time.</summary>
InstantRate,
/// <summary>Computed from other meters via a user-defined expression.</summary>
Virtual,
}
/// <summary>Whether a normalized amount is drawn (consumption) or produced (generation).</summary>
public enum ConsumptionKind : short
{
Consumption = 0,
Generation = 1,
}
/// <summary>Provenance/confidence of a reading or consumption row.</summary>
public enum ReadingQuality : short
{
Measured = 0,
Estimated = 1,
Manual = 2,
Imported = 3,
Interpolated = 4,
}
/// <summary>Bitmask annotations on a reading/consumption delta.</summary>
[Flags]
public enum ReadingFlags
{
None = 0,
CounterReset = 1,
MeterSwap = 2,
Anomaly = 4,
}
/// <summary>Discrete meter lifecycle/correction events (SDD meter_event.event_type).</summary>
public enum MeterEventType
{
MeterSwap,
CounterReset,
Delivery,
TankLevel,
Correction,
Note,
}
/// <summary>Where a meter_source pulls values from.</summary>
public enum SourceType
{
Mqtt,
Tasmota,
HomeAssistant,
Manual,
Import,
Virtual,
}
/// <summary>What kind of quantity a source reports (drives normalization).</summary>
public enum SourceValueKind
{
Register,
Delta,
Rate,
Level,
Runtime,
}
/// <summary>Scope a tariff applies to.</summary>
public enum TariffScope
{
Global,
EnergyType,
Meter,
}
/// <summary>Which price component a tariff row carries.</summary>
public enum TariffComponent
{
UnitPrice,
BasePrice,
FeedIn,
Bonus,
Discount,
Tax,
}
/// <summary>How a tank's runtime→consumption rate is derived.</summary>
public enum TankRateMode
{
Fixed,
Empirical,
}
/// <summary>Type of an ingestion endpoint (broker / HA connection).</summary>
public enum EndpointType
{
MqttBroker,
HomeAssistant,
}