M6-T06: extend MqttManager with subscribe + DSMR ingest
- mqtt.py: subscription registry, on_message dispatch (swallows handler errors), re-subscribe on (re)connect; publish/connect/disconnect/reconnect unchanged. - app/services/dsmr_ingest.py: handle_message stores the full telegram frame (no allowlist; gas/phases/null preserved), 10s downsample by timestamp.second, source_id idempotency (select + IntegrityError rollback). Net-thread safe. - main.py registers the DSMR subscription only when dsmr_ingest_enabled.
This commit is contained in:
@@ -18,12 +18,18 @@ Key design decisions
|
||||
- **Reconnect**: ``reconnect(settings)`` tears down the old client and
|
||||
establishes a fresh connection with the new settings. Callers (e.g. PUT
|
||||
/api/config) call this after saving MQTT-related config values.
|
||||
- **Subscribe**: ``subscribe(topic, handler)`` registers a topic → handler
|
||||
mapping. Subscriptions are re-established automatically on reconnect.
|
||||
The ``on_message`` callback dispatches incoming payloads to the registered
|
||||
handler; handler exceptions are caught and logged so they never crash the
|
||||
paho loop thread or the network connection.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
@@ -68,6 +74,9 @@ class MqttManager:
|
||||
self._client: mqtt.Client | None = None
|
||||
self._lock = threading.Lock()
|
||||
self._connected = False
|
||||
# topic → handler registry; persists across reconnects so subscriptions
|
||||
# are automatically re-established when the client reconnects.
|
||||
self._subscriptions: dict[str, Callable[[bytes], None]] = {}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public properties
|
||||
@@ -142,6 +151,30 @@ class MqttManager:
|
||||
except Exception:
|
||||
logger.exception("MQTT publish error (topic=%s).", topic)
|
||||
|
||||
def subscribe(self, topic: str, handler: Callable[[bytes], None]) -> None:
|
||||
"""Register *handler* to be called when a message arrives on *topic*.
|
||||
|
||||
If the client is already connected, the subscription is sent to the
|
||||
broker immediately. Otherwise it is queued and will be established
|
||||
the next time ``_on_connect`` fires (including after a reconnect).
|
||||
|
||||
Handler exceptions are swallowed by the ``on_message`` dispatcher so
|
||||
that a buggy handler can never crash the paho loop thread.
|
||||
"""
|
||||
self._subscriptions[topic] = handler
|
||||
# Grab the client reference outside the lock to avoid holding the lock
|
||||
# while calling back into paho (which may itself acquire internal locks).
|
||||
with self._lock:
|
||||
client = self._client
|
||||
connected = self._connected
|
||||
|
||||
if client is not None and connected:
|
||||
try:
|
||||
client.subscribe(topic)
|
||||
logger.debug("MQTT subscribed to topic=%s (immediate).", topic)
|
||||
except Exception:
|
||||
logger.exception("MQTT subscribe error (topic=%s).", topic)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers — must be called with self._lock held
|
||||
# ------------------------------------------------------------------
|
||||
@@ -168,6 +201,15 @@ class MqttManager:
|
||||
else:
|
||||
logger.info("MQTT connected (reason_code=%s).", reason_code)
|
||||
self._connected = True
|
||||
# Re-establish all registered subscriptions after (re-)connect.
|
||||
for sub_topic in self._subscriptions:
|
||||
try:
|
||||
_client.subscribe(sub_topic)
|
||||
logger.debug("MQTT re-subscribed to topic=%s.", sub_topic)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"MQTT re-subscribe failed (topic=%s).", sub_topic
|
||||
)
|
||||
|
||||
# VERSION2 on_disconnect signature:
|
||||
# (client, userdata, disconnect_flags, reason_code, properties)
|
||||
@@ -184,8 +226,29 @@ class MqttManager:
|
||||
else:
|
||||
logger.info("MQTT disconnected cleanly.")
|
||||
|
||||
# VERSION2 on_message signature: (client, userdata, message)
|
||||
def _on_message(
|
||||
_client: mqtt.Client,
|
||||
_userdata: object,
|
||||
message: mqtt.MQTTMessage,
|
||||
) -> None:
|
||||
handler = self._subscriptions.get(message.topic)
|
||||
if handler is None:
|
||||
logger.debug(
|
||||
"MQTT on_message: no handler for topic=%s.", message.topic
|
||||
)
|
||||
return
|
||||
try:
|
||||
handler(message.payload)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"MQTT message handler raised for topic=%s (swallowed).",
|
||||
message.topic,
|
||||
)
|
||||
|
||||
client.on_connect = _on_connect
|
||||
client.on_disconnect = _on_disconnect
|
||||
client.on_message = _on_message
|
||||
|
||||
# TLS
|
||||
if settings.mqtt_tls_enabled:
|
||||
|
||||
Reference in New Issue
Block a user