M3: live ingestion (MQTT/Tasmota + Home Assistant)

- PayloadExtractor: dot-path value/time extraction (Tasmota ENERGY.Total, bare scalars).
- MqttTopicMatcher: standard +/# wildcard matching.
- IngestionService: scale/offset, idempotent upsert on (meter_id, time), and a spurious-
  decrease guard for monotonic registers (allowed only with a reset/swap event) + source
  last-seen status.
- MqttMessageRouter + MqttIngestionWorker (MQTTnet 5): per-endpoint persistent connections,
  topic subscription, graceful degradation; secrets resolved by env-var reference.
- Home Assistant: HaStateClient (REST /api/states parse) + HomeAssistantWorker polling on
  each source's interval. HA-via-MQTT also works through the MQTT path.
- Ingestion workers gated by MeterVault:EnableLiveIngestion (off in tests).

85 tests green (53 Core + 32 integration): Tasmota payload → reading verified end to end.

Follow-up (polish): HA WebSocket push (state_changed) as an alternative to REST poll;
source-topic index caching in the router.

Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
This commit is contained in:
2026-07-13 11:45:20 +02:00
parent 5977c81002
commit 4b0cad67df
17 changed files with 1060 additions and 0 deletions
+15
View File
@@ -1,5 +1,6 @@
using MeterVault.Core.Normalization;
using MeterVault.Infrastructure.Import;
using MeterVault.Infrastructure.Ingestion;
using MeterVault.Infrastructure.Normalization;
using MeterVault.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
@@ -24,7 +25,21 @@ public static class DependencyInjection
services.AddScoped<NormalizationService>();
services.AddScoped<CsvImporter>();
services.AddScoped<ImportService>();
services.AddScoped<IngestionService>();
services.AddScoped<MqttMessageRouter>();
return services;
}
/// <summary>
/// Registers the live-ingestion background workers (MQTT/Tasmota + Home Assistant). Kept
/// separate from <see cref="AddMeterVaultInfrastructure"/> so tests can opt out of brokers.
/// </summary>
public static IServiceCollection AddMeterVaultIngestion(this IServiceCollection services)
{
services.AddHttpClient();
services.AddHostedService<MqttIngestionWorker>();
services.AddHostedService<HomeAssistantWorker>();
return services;
}
}