DSMR hardening: restart-free config + decouple from telegram id

Config hot-reload (no restart):
- MqttManager.unsubscribe(); apply_dsmr_subscription(settings) re-applies the
  DSMR subscription (subscribe/unsubscribe/topic-change, fresh snapshot so the
  sample interval also takes effect).
- Called from lifespan AND PUT /api/config, so toggling DSMR ingest in the UI
  takes effect immediately without restarting the app.

Decouple ingest from the DSMR telegram id (overflows / resets to zero):
- Migration 20260624_12: drop UNIQUE(source_id), make recorded_at UNIQUE.
- Idempotency now keys on recorded_at (telegram timestamp); the table's own
  autoincrement PK is the stable identity. source_id kept only as a reference.
- Regression test: colliding telegram ids no longer drop new data.
This commit is contained in:
2026-06-24 11:02:32 +02:00
parent 78f3cc4776
commit 8d4f496ff4
12 changed files with 385 additions and 61 deletions
+30
View File
@@ -107,6 +107,36 @@ def test_subscribe_when_connected_calls_paho_subscribe_immediately() -> None:
mock_client.subscribe.assert_called_once_with("dsmr/json")
def test_unsubscribe_removes_handler_from_registry() -> None:
"""unsubscribe() must drop the handler so it is not re-subscribed on reconnect."""
manager = MqttManager()
manager.subscribe("dsmr/json", MagicMock())
manager.unsubscribe("dsmr/json")
assert "dsmr/json" not in manager._subscriptions
def test_unsubscribe_when_connected_calls_paho_unsubscribe() -> None:
"""unsubscribe() while connected must call client.unsubscribe(topic)."""
manager = MqttManager()
mock_client = MagicMock()
manager._client = mock_client
manager._connected = True
manager.subscribe("dsmr/json", MagicMock())
manager.unsubscribe("dsmr/json")
mock_client.unsubscribe.assert_called_once_with("dsmr/json")
def test_unsubscribe_unknown_topic_is_noop() -> None:
"""unsubscribe() on a topic that was never registered must not raise."""
manager = MqttManager()
manager.unsubscribe("never/registered") # must not raise
assert "never/registered" not in manager._subscriptions
# ---------------------------------------------------------------------------
# on_message dispatch
# ---------------------------------------------------------------------------