Files
bellsystems-cp/.claude/backend-mqtt-alerts-prompt.md

5.0 KiB

Backend Task: Subscribe to Vesper MQTT Alert Topics

Use this document as a prompt / task brief for implementing the backend side of the Vesper MQTT alert system. The firmware changes are complete. Full topic spec: docs/reference/mqtt-events.md


What the firmware now publishes

The Vesper firmware (v155+) publishes on three status topics:

1. vesper/{device_id}/status/heartbeat (unchanged)

  • Every 30 seconds, retained, QoS 1
  • You already handle this — no change needed except: suppress any log entry / display update triggered by heartbeat arrival. Update last_seen silently. Only surface an event when the device goes silent (no heartbeat for 90s).

2. vesper/{device_id}/status/alerts (NEW)

  • Published only when a subsystem state changes (HEALTHY → WARNING, WARNING → CRITICAL, etc.)
  • QoS 1, not retained
  • One message per state transition — not repeated until state changes again

Alert payload:

{ "subsystem": "FileManager", "state": "WARNING", "msg": "ConfigManager health check failed" }

Cleared payload (recovery):

{ "subsystem": "FileManager", "state": "CLEARED" }

3. vesper/{device_id}/status/info (NEW)

  • Published on significant device state changes (playback start/stop, etc.)
  • QoS 0, not retained
{ "type": "playback_started", "payload": { "melody_uid": "ABC123" } }

What to implement in the backend (FastAPI + MQTT)

Subscribe to new topics

Add to your MQTT subscription list:

client.subscribe("vesper/+/status/alerts", qos=1)
client.subscribe("vesper/+/status/info",   qos=0)

Database model — active alerts per device

Create a table (or document) to store the current alert state per device:

CREATE TABLE device_alerts (
    device_id   TEXT NOT NULL,
    subsystem   TEXT NOT NULL,
    state       TEXT NOT NULL,    -- WARNING | CRITICAL | FAILED
    message     TEXT,
    updated_at  TIMESTAMP NOT NULL,
    PRIMARY KEY (device_id, subsystem)
);

Or equivalent in your ORM / MongoDB / Redis structure.

MQTT message handler — alerts topic

def on_alerts_message(device_id: str, payload: dict):
    subsystem = payload["subsystem"]
    state     = payload["state"]
    message   = payload.get("msg", "")

    if state == "CLEARED":
        # Remove alert from active set
        db.device_alerts.delete(device_id=device_id, subsystem=subsystem)
    else:
        # Upsert — create or update
        db.device_alerts.upsert(
            device_id  = device_id,
            subsystem  = subsystem,
            state      = state,
            message    = message,
            updated_at = now()
        )

    # Optionally push a WebSocket event to the console UI
    ws_broadcast(device_id, {"event": "alert_update", "subsystem": subsystem, "state": state})

MQTT message handler — info topic

def on_info_message(device_id: str, payload: dict):
    event_type = payload["type"]
    data       = payload.get("payload", {})

    # Store or forward as needed — e.g. update device playback state
    if event_type == "playback_started":
        db.devices.update(device_id, playback_active=True, melody_uid=data.get("melody_uid"))
    elif event_type == "playback_stopped":
        db.devices.update(device_id, playback_active=False, melody_uid=None)

API endpoint — get active alerts for a device

GET /api/devices/{device_id}/alerts

Returns the current active alert set (the upserted rows from the table above):

[
  { "subsystem": "FileManager",  "state": "WARNING",  "message": "SD mount failed",    "updated_at": "..." },
  { "subsystem": "TimeKeeper",   "state": "WARNING",  "message": "NTP sync failed",    "updated_at": "..." }
]

An empty array means the device is fully healthy (no active alerts).

Console UI guidance

  • Device list: show a coloured dot next to each device (green = no alerts, yellow = warnings, red = critical/failed). Update via WebSocket push.
  • Device detail page: show an "Active Alerts" section that renders the alert set statically. Do not render a scrolling alert log — just the current state.
  • When a CLEARED event arrives, remove the entry from the UI immediately.

What NOT to do

  • Do not log every heartbeat as a visible event. Heartbeats are internal housekeeping.
  • Do not poll the device for health status — the device pushes on change.
  • Do not store alerts as an append-only log — upsert by (device_id, subsystem). The server holds the current state, not a history.

Testing

  1. Flash a device with firmware v155+
  2. Subscribe manually:
    mosquitto_sub -h <broker> -t "vesper/+/status/alerts" -v
    mosquitto_sub -h <broker> -t "vesper/+/status/info"   -v
    
  3. Remove the SD card from the device — expect a FileManager WARNING alert within 5 minutes (next health check cycle), or trigger it immediately via:
    { "v": 2, "cmd": "system.health" }
    
    sent to vesper/{device_id}/control
  4. Reinsert the SD card — expect a FileManager CLEARED alert on the next health check