from __future__ import annotations import logging import re from typing import Any from sqlmodel import Session from app.core.config import settings from app.core.db import engine from app.mqtt.repository import insert_raw_message logger = logging.getLogger(__name__) TOPIC_PATTERN = re.compile(r"^terminal/(?P[^/]+)/raw$") def extract_device_id(topic: str) -> str: match = TOPIC_PATTERN.match(topic) if match is None: raise ValueError("topic must match terminal/{device_id}/raw") return match.group("device_id") def _on_connect( client: Any, _userdata: object, _flags: dict[str, int], reason_code: int, _properties: object | None = None, ) -> None: if reason_code != 0: logger.error("MQTT connect failed: rc=%s", reason_code) return logger.info( "Connected to MQTT broker %s:%s", settings.MQTT_HOST, settings.MQTT_PORT ) client.subscribe(settings.MQTT_TOPIC_PATTERN, qos=1) def _on_message( _client: Any, _userdata: object, message: Any, ) -> None: try: device_id = extract_device_id(message.topic) except ValueError: logger.warning("Skipping unexpected topic: %s", message.topic) return with Session(engine) as session: insert_raw_message( session=session, device_id=device_id, topic=message.topic, payload=bytes(message.payload), qos=message.qos, retain=message.retain, ) def create_client() -> Any: try: import paho.mqtt.client as mqtt # type: ignore[import-not-found,import-untyped] except ModuleNotFoundError as exc: # pragma: no cover - runtime guard raise RuntimeError( "paho-mqtt is required to run mqtt-ingestor. Install backend dependencies first." ) from exc client = mqtt.Client( callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=settings.MQTT_CLIENT_ID, clean_session=False, ) if settings.MQTT_USERNAME: client.username_pw_set(settings.MQTT_USERNAME, settings.MQTT_PASSWORD) client.on_connect = _on_connect client.on_message = _on_message client.reconnect_delay_set(min_delay=1, max_delay=30) return client def run() -> None: logging.basicConfig(level=logging.INFO) client = create_client() client.connect(settings.MQTT_HOST, settings.MQTT_PORT, keepalive=60) client.loop_forever() if __name__ == "__main__": run()