Commands
:::info Status
Implemented. Source: docs/user-device-mqtt-contract.md §8, docs/transmitter-mqtt-contract.md §7–§8, §11.
:::
Commands travel backend → device on the cmd topic; the device responds on
ack, correlated by req_id.
Envelope
Transmitter (no expiry field required, though play may carry one):
{"v":"1.0","req_id":"c1a2…","cmd":"ping","args":{},"ts":1789000100}
Stream player — extends the envelope with expires_at:
{"v":"1.0","req_id":"c1a2…","cmd":"play","args":{},
"ts":1789000100,"expires_at":1789000400}
Transmitter commands
cmd | args | Behavior |
|---|---|---|
ping | — | ack immediately (health probe / latency) |
play | {"stream_url"?, "source"?: "local", "broadcast_id"?, "prayer"?, "expires_at"?} | Start playback (URL overrides config). Also how azaan is delivered — see Azan delivery below |
stop | — | Stop playback → ONLINE |
play_test | {"seconds": 5} | Short local test tone, for install verification |
set_volume | {"volume": 0-100} | Transient — the config publish is the durable path |
reboot | — | Ack first, then graceful OFFLINE(reason:reboot) + restart |
update_firmware | {"url": "...", "sha256": "..."} | OTA; status UPDATING with progress |
get_state | — | Ack with full current status payload (pull-based refresh) |
identify | {"seconds": 10} | Blink LED / chirp — find the physical unit |
An admin triggers these from POST /api/v1/mosque-devices/{device_id}/cmd —
the backend opens a short-lived MQTT session, publishes, waits for the
matching req_id on ack, and returns the result (or timeout) over HTTP.
Ack timeout: DEVICE_CMD_ACK_TIMEOUT_SECS, default 5s.
Stream player commands
All of the transmitter commands above, plus:
cmd | args | Behavior |
|---|---|---|
mute | {"until"?: epoch} | → MUTED; until absent means indefinite |
unmute | — | → ONLINE |
unbind_mosque | {"reason": "membership_lapsed"|"owner_rebind"|"unassigned"} | Stop azaan, drop mosque_id/stream_url, publish UNBOUND. Keeps device_secret, broker credentials, and Bluetooth. One config publish away from playing again — see Device Lifecycle |
bluetooth_pairing | {"open": true, "seconds": 120} | Open/close pairing mode |
sync_config | — | Re-request retained config; ack with current config_version |
set_log_level | {"level":"error|warn|info|debug", "seconds": 900} | Temporary log escalation; self-expires on the device |
diag_bundle | — | One-shot diagnostic snapshot |
deauthorize | {"reason": "revoked"|"blocked"|"retired"} | Decommission: stop azaan, wipe device_secret + broker credentials + retained config from NVS, disconnect, return to setup mode. Ack before wiping. Bluetooth keeps working. |
unbind_mosque and deauthorize are never interchangeable — one is a
routine move, the other is a rare, terminal decommission:
unbind_mosque | deauthorize | |
|---|---|---|
| Triggered by | owner left the mosque, owner rebinding, superadmin unassign | revoke, block, retire |
| Frequency | routine — every mosque change | rare, terminal |
device_secret + broker credentials | kept | wiped |
| Broker session | stays connected | disconnects |
| Path back | one config publish | full re-claim |
| Bluetooth | unaffected | unaffected |
Acks
{
"v": "1.0",
"req_id": "c1a2…",
"cmd": "play",
"result": "ok",
"error": null,
"data": {},
"ts": 1789000101
}
result: ok | error | unsupported | refused | expired.
unsupported— unknown command; never crash, never silent-drop.refused— the device understood the command and deliberately declined: quiet hours active, user mute, prayer disabled,azan_autoplayoff, or (a stream player) Bluetooth actively playing withazan_interrupts:false. Carries areasonindata, e.g.{"cmd":"play","result":"refused","data":{"reason":"quiet_hours"}}. Recorded in the broadcast audit as an intentional non-delivery, distinct fromerror— see Broadcast Audit.expired—expires_atalready passed; do nothing.
Azan delivery
Azaan is delivered as an ordinary play on the device's own cmd topic —
there is no mosque-wide trigger topic. core/device_trigger.py's
publish_play_to_transmitters (transmitters) and publish_azaan (stream
players) send it, re-running authorization at publish time (not relying on
a config published earlier) — see Device Authorization.
{"v":"1.0","req_id":"c1a2…","cmd":"play",
"args":{"stream_url":"https://…/{mosqueId}_azaan","source":"stream",
"broadcast_id":"…","prayer":"fajr"},
"ts":1789000100,"expires_at":1789000280}
stream_urlis always the mosque's live azaan Icecast mount — never a per-trigger uploaded file. See Azaan Broadcast for what actually streams into that mount (live mic, or the scheduler's file stream).expires_at: refuse (ack "expired") rather than execute a redelivery after the window passes.AZAAN_PLAY_TTL_SECONDS, default 180s, is the same window both device types use.broadcast_idpresence is the signal this is an automatic azaan (a transmitter checksazan_autoplay; a stream player checksprayers,quiet_hours, mute). Aplaywith nobroadcast_idis a manual command (admin device drawer, orplay_test) and always executes.
After playback, the device acks with "cmd":"azan_played":
{
"v": "1.0",
"req_id": null,
"cmd": "azan_played",
"result": "ok",
"data": {
"trigger_id": "…",
"broadcast_id": "…",
"prayer": "maghrib",
"duration_s": 178,
"source": "stream",
"wakeup": "mqtt"
},
"ts": 1789000280
}
source: "stream" (played the live mount) or "local_fallback" (the
flashed local azan played instead, because the stream failed and
fallback_local_audio is enabled) — fallback plays are surfaced loudly in
the audit. See Broadcast Audit.
Neither the general "live broadcast" room (e.g. a khutbah) nor any other
manual trigger type (jummah, waaz, taraweeh, announcement, test)
reaches a transmitter via this mechanism.