Add status/LWT to community MQTT ingest

This commit is contained in:
Jack Kingsman
2026-03-02 23:10:25 -08:00
parent fb279ccf1a
commit 4c1d5fb8ec
4 changed files with 214 additions and 2 deletions
+43
View File
@@ -20,6 +20,7 @@ import time
from datetime import datetime
from typing import Any
import aiomqtt
import nacl.bindings
from app.models import AppSettings
@@ -252,6 +253,12 @@ def _format_raw_packet(data: dict[str, Any], device_name: str, public_key_hex: s
return packet
def _build_status_topic(settings: AppSettings, pubkey_hex: str) -> str:
"""Build the ``meshcore/{IATA}/{PUBKEY}/status`` topic string."""
iata = settings.community_mqtt_iata.upper().strip()
return f"meshcore/{iata}/{pubkey_hex}/status"
class CommunityMqttPublisher(BaseMqttPublisher):
"""Manages the community MQTT connection and publishes raw packets."""
@@ -308,6 +315,15 @@ class CommunityMqttPublisher(BaseMqttPublisher):
tls_context = ssl.create_default_context()
status_topic = _build_status_topic(settings, pubkey_hex)
offline_payload = json.dumps(
{
"status": "offline",
"origin_id": pubkey_hex,
"client": _CLIENT_ID,
}
)
return {
"hostname": broker_host,
"port": broker_port,
@@ -316,6 +332,7 @@ class CommunityMqttPublisher(BaseMqttPublisher):
"websocket_path": "/",
"username": f"v1_{pubkey_hex}",
"password": jwt_token,
"will": aiomqtt.Will(status_topic, offline_payload, retain=True),
}
def _on_connected(self, settings: AppSettings) -> tuple[str, str]:
@@ -323,6 +340,32 @@ class CommunityMqttPublisher(BaseMqttPublisher):
broker_port = settings.community_mqtt_broker_port or _DEFAULT_PORT
return ("Community MQTT connected", f"{broker_host}:{broker_port}")
async def _on_connected_async(self, settings: AppSettings) -> None:
"""Publish a retained online status message after connecting."""
from app.keystore import get_public_key
from app.radio import radio_manager
public_key = get_public_key()
if public_key is None:
return
pubkey_hex = public_key.hex().upper()
device_name = ""
if radio_manager.meshcore and radio_manager.meshcore.self_info:
device_name = radio_manager.meshcore.self_info.get("name", "")
status_topic = _build_status_topic(settings, pubkey_hex)
payload = {
"status": "online",
"timestamp": datetime.now().isoformat(),
"origin": device_name or "MeshCore Device",
"origin_id": pubkey_hex,
"client": _CLIENT_ID,
}
await self.publish(status_topic, payload, retain=True)
def _on_error(self) -> tuple[str, str]:
return (
"Community MQTT connection failure",
+10 -2
View File
@@ -79,12 +79,12 @@ class BaseMqttPublisher(ABC):
await self.stop()
await self.start(settings)
async def publish(self, topic: str, payload: dict[str, Any]) -> None:
async def publish(self, topic: str, payload: dict[str, Any], *, retain: bool = False) -> None:
"""Publish a JSON payload. Drops silently if not connected."""
if self._client is None or not self.connected:
return
try:
await self._client.publish(topic, json.dumps(payload))
await self._client.publish(topic, json.dumps(payload), retain=retain)
except Exception as e:
logger.warning("%s publish failed on %s: %s", self._log_prefix, topic, e)
self.connected = False
@@ -124,6 +124,13 @@ class BaseMqttPublisher(ABC):
"""Called each time the loop finds the publisher not configured."""
return # no-op by default; subclasses may override
async def _on_connected_async(self, settings: AppSettings) -> None:
"""Async hook called after connection succeeds (before health broadcast).
Subclasses can override to publish messages immediately after connecting.
"""
return # no-op by default
# ── Connection loop ────────────────────────────────────────────────
async def _connection_loop(self) -> None:
@@ -170,6 +177,7 @@ class BaseMqttPublisher(ABC):
title, detail = self._on_connected(settings)
broadcast_success(title, detail)
await self._on_connected_async(settings)
_broadcast_health()
# Wait until cancelled or settings version changes.