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
+21
View File
@@ -175,6 +175,27 @@ class MqttManager:
except Exception:
logger.exception("MQTT subscribe error (topic=%s).", topic)
def unsubscribe(self, topic: str) -> None:
"""Remove the handler for *topic* and unsubscribe from the broker.
Idempotent: unknown topics are ignored. Used to apply config changes
without a restart (e.g. when DSMR ingest is turned off or its topic
changes). If the client is connected, the broker unsubscribe is sent
immediately; either way the handler is removed from the registry so it
will not be re-subscribed on the next ``_on_connect``.
"""
self._subscriptions.pop(topic, None)
with self._lock:
client = self._client
connected = self._connected
if client is not None and connected:
try:
client.unsubscribe(topic)
logger.debug("MQTT unsubscribed from topic=%s.", topic)
except Exception:
logger.exception("MQTT unsubscribe error (topic=%s).", topic)
# ------------------------------------------------------------------
# Internal helpers — must be called with self._lock held
# ------------------------------------------------------------------