Repository navigation
⭐ Meter Simulator
A .NET Worker Service replaying a real household power-consumption dataset across a configurable city block (both sides of a street — however many meters the deployment's BuildingsPerSide value produces) and publishing each reading to Kafka's meter.readings.raw topic as an Avro-encoded event, stamped with its resolved accountId, one reading per meter every 5 seconds.
-
MeterReading— an immutable record matching the design doc's event contract (meterId,timestamp,kwh,readingId) -
HouseholdPowerDataset— loads the embedded real UCI dataset excerpt once at startup -
MeterReadingGenerator— replays the real dataset, one independent cursor per meter (seeded by a hash ofmeterIdso every meter shows different real values from the same real month rather than identical echoes), converting real kilowatts to a real per-tick kWh amount -
MeterSimulatorOptions+CityBlockMeterIdFactory(from the sharedGridPulse.Sharedlibrary, also used by Account/Customer Service — seedocs/architecture.md) — config in, meter-ID list out (see below) -
CensusAddressValidator— confirms the configured address is real against the live U.S. Census Geocoding API before the app is allowed to start -
AccountResolver/AccountLookupCache— resolves each configured meter'saccountIdfrom Account/Customer Service once at startup (fail-fast if any meter isn't registered), same pattern as the Census check -
Worker— aBackgroundServicethat composes the above on a timer, publishing each reading (keyed byMeterId, preserving per-meter partition ordering, stamped with itsaccountId) viaIProducer<string, MeterReadingRaw>and a real Confluent Schema Registry
Readings were purely synthetic since Phase 1 (a random 0-5 kWh per tick). Replaced with a replay of a real, published dataset — UCI's Individual Household Electric Power Consumption — real minute-level power measurements from one real household. A monthly GitHub Actions job (refresh-power-dataset.yml) cycles the checked-in excerpt through the real archive's full December 2006–November 2010 span over time, since the full ~127MB archive exceeds GitHub's 100MB file limit. Full reasoning, including the kW→kWh conversion and per-meter cursor design, is in todo.md.
StreetName, StartingAddress, BuildingsPerSide, City, and ZipCode are [Required]/[Range]-validated with no default values — the app refuses to start with a clean error (not a stack trace) if any are missing. CityBlockMeterIdFactory computes the actual meter-ID list (even side + odd side) from those numbers at startup, rather than a literal list living in source.
On top of that, CensusAddressValidator calls the real U.S. Census Bureau Geocoding API at startup to confirm the composed address ("{StartingAddress} {StreetName}, {City}, MI {ZipCode}") actually exists — a fictitious or mismatched street/city/zip refuses to start the app, not just a missing one.
AppHost supplies all five via Aspire's builder.AddParameter(...) (itself with no default), which resolves from dotnet user-secrets — never a file that gets committed. A real field deployment wouldn't go through AppHost at all; it'd set the same config as real OS/container environment variables.
Full reasoning — including several real self-corrections and bugs caught along the way — is in todo.md.
Meter Simulator no longer has any reference to Usage Aggregation at all — not in code, not in AppHost.cs. It publishes to a topic and doesn't know or care who (if anyone) is listening, exactly the point of an event-driven core loop over direct service-to-service calls.
Ran the full AppHost and confirmed via captured process output: 924 of 924 readings published successfully to Kafka in one run, each with a unique ReadingId. A real startup-race bug was caught this way — the schema registry container reached "Running" before its HTTP server was actually ready to accept requests, causing early sends to fail; fixed with a proper HTTP health check on that resource so WaitFor genuinely waits for readiness. Re-verified after switching to the real dataset: genuinely different, gradually-changing real values landing per meter (not random jumps), confirmed directly in Postgres. Re-verified again after Phase 3's accountId resolution: every reading's accountId a real seeded account GUID, confirmed via a real fresh Kafka consumer.
15 xUnit tests cover MeterReadingGenerator (including a deterministic test asserting the exact kW→kWh conversion against a known real dataset value), MeterSimulatorOptions fail-fast validation, and CensusAddressValidator (mocked match/no-match/error responses, no real network calls in tests). CityBlockMeterIdFactory's own tests moved to Shared.Tests when it was extracted into GridPulse.Shared.