MQTT Topics

Reference for every topic a thesada-fw device touches. Use this when wiring up dashboards, building integrations, or auditing what the device sends.

Topic prefix

Each device has a configured topic prefix that anchors every topic it owns. The prefix lives at mqtt.topic_prefix in config.json; default is thesada/node.

Two shapes are common in deployments:

Shape Example prefix When to use
3-tier legacy thesada/owb Single-tenant deployments. Older firmware defaults.
4-tier thesada/<tenant>/<device> Multi-tenant deployments. Tenant slug is the second segment, device name third.

The firmware does not care which shape you pick - the prefix is opaque to it. The wider Thesada platform parses 4-tier prefixes to route by tenant; standalone deployments work fine with 3-tier.

In every section below, <prefix> stands for whatever this device’s prefix is.

At a glance

Topic Direction Retained QoS When
<prefix>/status publish yes 0 LWT offline; online on connect
<prefix>/info publish yes 0 Once per connect
<prefix>/info/retained_topics publish yes 0 Once per connect, debounced re-emit on late additions
<prefix>/sensor/<kind>/<name> publish no 0 Per sensor read interval
<prefix>/alert publish no 0 Per fired alert
<prefix>/status/ota publish no 0 OTA progress / outcome
<prefix>/cellular/active publish no 0 Cellular fallback toggled on
<prefix>/cellular/rssi publish no 0 Periodic during cellular session
<prefix>/cli/<command> subscribe no 0 Operator CLI invocation
<prefix>/cli/response publish no 0 Reply to a CLI invocation
<prefix>/cmd/lua/reload subscribe no 0 Hot-reload Lua scripts
homeassistant/<component>/<dev>/<uid>/config publish yes 0 HA autodiscovery, once per connect

QoS 0 throughout: the Thesada device fleet trades guaranteed delivery for connection liveness. The firmware uses retained for state-of-the-world topics so a fresh subscriber catches up, and uses LWT for offline detection.

Lifecycle topics

<prefix>/status

Online / offline marker. The firmware sets the broker’s Last Will and Testament to publish offline (retained) on disconnect, then publishes online (retained) on every successful connect. Subscribers see one of two payloads at any moment:

online
offline

<prefix>/info

Retained JSON blob describing the device. Published once per successful connect; the retained record gives a fresh subscriber the full device metadata immediately.

{
  "firmware_version": "1.3.11",
  "hardware_type": "esp32-s3",
  "board": "s3-bare",
  "chip_model": "esp32-s3",
  "chip_revision": 0,
  "chip_cores": 2,
  "mac": "48:27:e2:e8:b4:34",
  "psram": true,
  "build_time": "Apr 30 2026 14:45:01",
  "config_hash": "a3b1...e2",
  "scripts_main_hash": "c4d2...8f",
  "scripts_rules_hash": "0000...00"
}

The three hash fields are SHA-256 of the live config and the Lua scripts. Use them for drift detection: if the hash you see on the broker disagrees with what your management database expects, the device is running a different config or script set than you think.

<prefix>/info/retained_topics

Retained JSON array listing every topic this device currently retains on the broker. Built fresh on each connect; debounce-republished if a module retains a new topic after the initial connect.

[
  "thesada/sht31/status",
  "thesada/sht31/info",
  "thesada/sht31/info/retained_topics",
  "homeassistant/sensor/sht31/sht31_sht31_temp/config",
  "homeassistant/sensor/sht31/sht31_sht31_humidity/config"
]

The point: when a device is decommissioned, the retained payloads it owns persist on the broker forever (that is what retained means). A management process can read this manifest, then publish empty retained payloads on each listed topic to clean up.

Sensor topics

<prefix>/sensor/<kind>/<name> for per-sensor live readings. Payload is the bare value as a string; subscribers parse it as a number when they expect one.

Topic shape Example Payload
<prefix>/sensor/temperature/<name> <prefix>/sensor/temperature/boiler 21.4
<prefix>/sensor/humidity/<name> <prefix>/sensor/humidity/sht31 54.2
<prefix>/sensor/current/<name> <prefix>/sensor/current/house_pump 2.31
<prefix>/sensor/power/<name> <prefix>/sensor/power/house_pump 277.2
<prefix>/sensor/battery/percent (singleton) 87
<prefix>/sensor/battery/voltage (singleton) 4.012
<prefix>/sensor/battery/charging (singleton) Charging / Discharging
<prefix>/sensor/wifi/rssi (singleton) -54
<prefix>/sensor/wifi/ssid (singleton) MyWiFi
<prefix>/sensor/wifi/ip (singleton) 192.168.1.50
<prefix>/sensor/eth/ip (singleton, ETH boards) 10.0.0.42
<prefix>/sensor/eth/speed (singleton, ETH boards) 100
<prefix>/sensor/eth/mac (singleton, ETH boards) 8c:4f:00:11:22:33
<prefix>/sensor/heap/free (singleton) 123456
<prefix>/sensor/heap/min_free (singleton) 89012
<prefix>/sensor/heap/max_alloc_block (singleton) 38400
<prefix>/sensor/heap/psram_free (singleton, PSRAM boards) 4194304
<prefix>/sensor/uptime (singleton) 12345
<prefix>/sensor/pwm (singleton, PWM boards) 45
<prefix>/cellular/rssi (singleton, cellular boards) -83

Sensors are not retained: the value at any moment is the most recent publish, and a subscriber that joined late reads the next interval. Heap stats publish every 5 minutes; sensor reads follow each module’s configured interval (sensors[].interval_s, sht31.interval_s, etc).

The current/<name> and power/<name> topics from the ADS1115 module also publish a roll-up payload on <prefix>/sensor/current and <prefix>/sensor/power for boards that aggregate.

Names come from the device’s config.json. A sensor named “House Pump” in config slugifies to house_pump for the topic. Renaming a sensor changes the topic, which is intentional - the topic IS the name.

Alerts

<prefix>/alert

JSON payload, fired when a Lua alert rule matches.

{
  "device": "owb",
  "alert_id": "boiler_overtemp",
  "severity": "crit",
  "message": "Boiler 78.4 C above 75 C threshold",
  "metric": "temperature.boiler",
  "value": 78.4,
  "ts": 1735000000,
  "free_heap": 184320
}

Severity is one of info, warn, crit. The free_heap field is the firmware’s last sampled free heap at alert-publish time; useful when triaging whether an alert misfired during a low-heap period.

Cellular boards mirror alerts on the same topic via the cellular module’s queue when WiFi is down, so subscribers do not need a separate cellular alert path.

<prefix>/status/ota

Plain-string OTA progress events.

checking
downloading
verifying
applied: 1.3.11
failed: connection_lost

Useful for live OTA dashboards. Not retained; rolling status is captured on the broker only while the OTA is happening.

CLI bridge

The firmware subscribes to <prefix>/cli/# so any topic under it is treated as a command. Topic suffix is the command name; payload is the argument string.

Inbound: <prefix>/cli/<command>

mosquitto_pub -t 'thesada/owb/cli/chip.info' -m ''
mosquitto_pub -t 'thesada/owb/cli/ota.check' -m '--force'
mosquitto_pub -t 'thesada/owb/cli/config.set' -m 'sleep.deep_sleep_minutes 15'

Some commands take multi-line payloads (fs.write, fs.append, cert.set); see the CLI Reference for the wire contracts.

Outbound: <prefix>/cli/response

JSON envelope with the original command, success flag, and one entry per output line.

{
  "cmd": "chip.info",
  "ok": true,
  "output": [
    "ESP32-S3 rev 0  cores=2  flash=8 MB",
    "PSRAM: 8 MB octal SPI"
  ]
}

Subscribe before publishing to avoid missing the response (it is QoS 0 + not retained).

<prefix>/cmd/lua/reload

The Lua module also subscribes to a fixed reload trigger separate from the CLI bridge. Publishing any payload (empty works) re-runs every script under /scripts/ in load order. Equivalent to issuing lua.reload via the CLI.

Home Assistant discovery

When mqtt.ha_discovery: true in config.json (default), the firmware publishes Home Assistant MQTT autodiscovery configs (retained) on connect. Topic shape:

homeassistant/<component>/<device_id>/<unique_id>/config

<component> is the HA entity component (sensor, binary_sensor, etc - currently only sensor is used). <device_id> is the device’s name from config.json. <unique_id> is per-entity, derived from the device id and a per-sensor suffix.

Each config payload references the sensor’s state topic from the table above and an availability topic of <prefix>/status, so HA marks the entity unavailable when the device drops offline. Worked-out example for an SHT31 humidity sensor on a device named sht31:

{
  "name": "SHT31 Humidity",
  "stat_t": "thesada/sht31/sensor/humidity/sht31",
  "uniq_id": "sht31_sht31_humidity",
  "avty_t": "thesada/sht31/status",
  "unit_of_meas": "%",
  "dev_cla": "humidity",
  "stat_cla": "measurement",
  "dev": {
    "ids": "sht31",
    "name": "SHT31 Test Node",
    "mf": "Thesada",
    "sw": "1.3.11"
  }
}

The firmware emits one of these per active sensor on connect: temperature, humidity, current, power, battery (percent / voltage / charging), wifi (rssi / ssid / ip), eth (ip / speed / mac), heap (free / min / max_alloc / psram), uptime. Disabling a module via build flags removes its discovery configs from the published set.

Authentication and ACLs

The firmware connects with one of:

  • Username + password from config.json mqtt.user / mqtt.password. The fallback path used by unpaired devices.
  • mTLS client certificate stored in NVS. When present, cert.apply triggers a reconnect using the cert as identity (no username, no password). The broker maps the certificate CN onto the MQTT username via use_identity_as_username, so the connection arrives as if the device had logged in with username = CN.

ACLs live on the broker side via the Mosquitto dynamic-security plugin. Per-device pairing creates a role scoped to <prefix>/# plus the device’s homeassistant/... discovery namespace. Operators wire those out of band; the firmware does not configure ACLs.

Putting it together

A typical subscribe pattern for a single device:

mosquitto_sub -v -t 'thesada/owb/#'

For Home Assistant discovery on a single device:

mosquitto_sub -v -t 'homeassistant/sensor/owb/#'

For a tenanted multi-device deployment, the wider thesada/# works at the broker layer; combine with broker-side ACLs to scope by tenant.

To inspect what one device retains right now:

mosquitto_sub -t 'thesada/owb/info/retained_topics' -W 5 -C 1 | jq .

Thesada - GPL-3.0-only (firmware) / CC BY-NC-SA 4.0 (docs) - License

This site uses Just the Docs, a documentation theme for Jekyll.