feat: add MQTT integration for real-time entity updates

- aiomqtt async client with auto-reconnect and topic store
- MQTT router: GET /api/mqtt, GET /api/mqtt/topic/{path}, POST /api/mqtt/publish
- MQTT entities included in /api/all + WebSocket broadcast
- MqttCard frontend component with category filters, entity list
- Configurable via ENV: MQTT_HOST, MQTT_PORT, MQTT_USERNAME,
  MQTT_PASSWORD, MQTT_TOPICS (comma-separated or JSON array)
- Gracefully disabled when MQTT_HOST is not set

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam 2026-03-02 10:13:50 +01:00
parent 9f7330e217
commit 89ed0c6d0a
11 changed files with 542 additions and 1 deletions

View file

@ -48,6 +48,14 @@ class Settings:
news_cache_ttl: int = 300 # 5 min
news_max_age_hours: int = 48
# --- MQTT ---
mqtt_host: str = ""
mqtt_port: int = 1883
mqtt_username: str = ""
mqtt_password: str = ""
mqtt_topics: List[str] = field(default_factory=lambda: ["#"])
mqtt_client_id: str = "daily-briefing"
# --- Server ---
host: str = "0.0.0.0"
port: int = 8080
@ -73,6 +81,20 @@ class Settings:
s.vikunja_url = os.getenv("VIKUNJA_URL", s.vikunja_url)
s.vikunja_token = os.getenv("VIKUNJA_TOKEN", s.vikunja_token)
s.mqtt_host = os.getenv("MQTT_HOST", s.mqtt_host)
s.mqtt_port = int(os.getenv("MQTT_PORT", str(s.mqtt_port)))
s.mqtt_username = os.getenv("MQTT_USERNAME", s.mqtt_username)
s.mqtt_password = os.getenv("MQTT_PASSWORD", s.mqtt_password)
s.mqtt_client_id = os.getenv("MQTT_CLIENT_ID", s.mqtt_client_id)
# Parse MQTT_TOPICS (comma-separated or JSON array)
raw_topics = os.getenv("MQTT_TOPICS", "")
if raw_topics:
try:
s.mqtt_topics = json.loads(raw_topics)
except (json.JSONDecodeError, TypeError):
s.mqtt_topics = [t.strip() for t in raw_topics.split(",") if t.strip()]
s.debug = os.getenv("DEBUG", "").lower() in ("1", "true", "yes")
# Parse UNRAID_SERVERS JSON