Skip to main content

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

cmdargsBehavior
pingack 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
stopStop 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
rebootAck first, then graceful OFFLINE(reason:reboot) + restart
update_firmware{"url": "...", "sha256": "..."}OTA; status UPDATING with progress
get_stateAck 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:

cmdargsBehavior
mute{"until"?: epoch}MUTED; until absent means indefinite
unmuteONLINE
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_configRe-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_bundleOne-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_mosquedeauthorize
Triggered byowner left the mosque, owner rebinding, superadmin unassignrevoke, block, retire
Frequencyroutine — every mosque changerare, terminal
device_secret + broker credentialskeptwiped
Broker sessionstays connecteddisconnects
Path backone config publishfull re-claim
Bluetoothunaffectedunaffected

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_autoplay off, or (a stream player) Bluetooth actively playing with azan_interrupts:false. Carries a reason in data, e.g. {"cmd":"play","result":"refused","data":{"reason":"quiet_hours"}}. Recorded in the broadcast audit as an intentional non-delivery, distinct from error — see Broadcast Audit.
  • expiredexpires_at already 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_url is 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_id presence is the signal this is an automatic azaan (a transmitter checks azan_autoplay; a stream player checks prayers, quiet_hours, mute). A play with no broadcast_id is a manual command (admin device drawer, or play_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.