An MQTT-based bridge server for managing Tuya devices via rustuya.
Standalone mode: If
--mqtt-brokeris omitted, the bridge runs in debug/standalone mode — devices are still tracked and persisted to the state file, but no MQTT publish/subscribe occurs. A warning is logged at startup.
Download the archive for your platform from the Releases page, extract, and run:
tar -xzf rustuya-bridge-<target>.tar.gz
./rustuya-bridge --mqtt-broker mqtt://localhost:1883| Platform | Target |
|---|---|
| Linux x86_64 | x86_64-unknown-linux-musl |
| Linux ARM64 (RPi 4/5, ARM servers) | aarch64-unknown-linux-musl |
| Linux ARMv7 (RPi 3, Zero 2 W) | armv7-unknown-linux-musleabihf |
| macOS Intel | x86_64-apple-darwin |
| macOS Apple Silicon | aarch64-apple-darwin |
| Windows | x86_64-pc-windows-msvc |
For long-running deployments, run the binary under a process supervisor such as systemd (Linux), launchd (macOS), or supervisord so that the bridge restarts automatically on crash and starts at boot.
scripts/bridgectl.sh installs the latest release,
creates a dedicated rustuya user, writes a default config, registers the
systemd unit, and starts the service. It also copies itself to
/usr/local/bin/bridgectl so subsequent management runs from anywhere.
curl -fsSL https://raw.githubusercontent.com/3735943886/rustuya-bridge/master/scripts/bridgectl.sh \
| sudo bash -s -- install --yesThe service starts immediately with mqtt://localhost:1883 as the default
broker. To point it at your own broker (or change topics, retain, log level,
etc.), edit the auto-generated config and restart:
sudo nano /var/lib/rustuya/config.json # see Configuration File below for fields
sudo systemctl restart rustuya-bridge
journalctl -u rustuya-bridge -f # tail logsSubsequent management:
bridgectl # show status / latest version / service state
sudo bridgectl upgrade # pull the latest release and restart
sudo bridgectl remove # stop + uninstall (keeps data dir and user)
sudo bridgectl purge # also wipe data dir, user, and the helper itself
bridgectl help # full command + option referenceAll install / upgrade / remove / purge accept --yes (-y) to skip
confirmation prompts — required when running non-interactively (e.g. via
curl | sudo bash).
bridgectl defaults to the stable channel (GitHub's releases/latest,
which excludes pre-releases). Pass --prerelease to target the newest
release including -rc / -alpha / -beta builds:
sudo bridgectl install --prerelease # install the latest RC
sudo bridgectl upgrade --prerelease # upgrade to a newer RC
bridgectl status --prerelease # show the newest RC vs installedstatus and upgrade are semver-aware: if you have an RC installed and
run them without --prerelease, the channel's latest stable is reported
honestly, but the prompt is reframed as a downgrade so you don't yes
through a regression. To roll back from a problematic RC to the last
stable build:
sudo bridgectl upgrade # detects RC > stable, prompts "Downgrade ... ?"Clone and run directly with cargo, or produce a release binary you can copy elsewhere:
git clone https://github.com/3735943886/rustuya-bridge
cd rustuya-bridge
# One-shot run (development)
cargo run --release -- --mqtt-broker mqtt://localhost:1883
# Build only — binary at ./target/release/rustuya-bridge
cargo build --release
./target/release/rustuya-bridge --mqtt-broker mqtt://localhost:1883Run with host network mode to ensure Tuya device discovery works correctly:
docker run -d \
--name rustuya \
--network host \
-e MQTT_BROKER=mqtt://localhost:1883 \
-v $(pwd)/data:/data \
3735943886/rustuyaThe bridge can be configured via command-line arguments or environment variables:
| Argument | Environment Variable | Default | Description |
|---|---|---|---|
--config, -C |
CONFIG |
- | Path to a JSON configuration file |
--mqtt-broker, -m |
MQTT_BROKER |
- | MQTT Broker address (e.g., mqtt://user:pass@localhost:1883) |
--mqtt-root-topic |
MQTT_ROOT_TOPIC |
rustuya |
MQTT root topic prefix |
--mqtt-command-topic |
MQTT_COMMAND_TOPIC |
{root}/command |
MQTT topic for commands |
--mqtt-event-topic |
MQTT_EVENT_TOPIC |
{root}/event/{type}/{id} |
MQTT topic for events |
--mqtt-scanner-topic |
MQTT_SCANNER_TOPIC |
{root}/scanner |
MQTT topic for scanner results |
--mqtt-client-id |
MQTT_CLIENT_ID |
rustuya-bridge |
MQTT client identifier |
--mqtt-message-topic |
MQTT_MESSAGE_TOPIC |
{root}/{level}/{id} |
MQTT topic for errors/responses (e.g., tuya/logs/{level}) |
--mqtt-payload-template |
MQTT_PAYLOAD_TEMPLATE |
{value} |
MQTT payload template (e.g., {"val": {value}}) |
--mqtt-retain |
MQTT_RETAIN |
false |
Retain flag for device state messages |
--state-file, -s |
STATE_FILE |
rustuya.json |
Path to the file where device snapshots are stored |
--save-debounce-secs |
SAVE_DEBOUNCE_SECS |
30 |
Seconds to wait before saving state file (debounce) |
--scavenger-timeout-secs |
SCAVENGER_TIMEOUT_SECS |
1 |
Seconds the retain scavenger waits for retained MQTT messages before exiting after remove/clear. Raise on slow brokers. |
--log-level, -l |
LOG_LEVEL |
info |
Log level: error, warn, info, debug, trace |
A JSON file can be used to manage all settings. Command-line arguments take priority over settings in the config file.
config.json example:
{
"mqtt_broker": "mqtt://localhost:1883",
"mqtt_root_topic": "myhome/tuya",
"mqtt_event_topic": "tuya/{name}/{dp}/state",
"mqtt_payload_template": "{value}",
"save_debounce_secs": 10
}Run with:
./rustuya-bridge --config config.json- Commands: Publish a JSON payload to the
mqtt-command-topic. - Events: Device events are published to the
mqtt-event-topic.- Active: Published when the payload contains
dpsdata (e.g., state changes). - Passive: Published when the payload contains no
dpsdata (e.g., state reports).
- Active: Published when the payload contains
- Responses/Errors: Command results and errors are published to
mqtt-message-topic.- Success: Published with
{level}set toresponse. - Error: Published with
{level}set toerror.
- Success: Published with
- Scanner: Results are published to
mqtt-scanner-topic.
You can interact with the bridge using any MQTT client (e.g., mosquitto_pub/sub).
By default, the command topic is rustuya/command and events are published under rustuya/event/#.
Before running the commands below, you might want to open a new terminal and subscribe to all topics:
mosquitto_sub -h localhost -t "rustuya/#" -vSubscribe specifically to command responses:
mosquitto_sub -h localhost -t "rustuya/response/#" -vmosquitto_pub -h localhost -t "rustuya/command" -m '{
"action": "add",
"id": "device_id",
"key": "local_key",
"ip": "device_ip"
}'mosquitto_pub -h localhost -t "rustuya/command" -m '{
"action": "add",
"id": "sub_device_id",
"parent_id": "gateway_device_id",
"cid": "sub_device_cid"
}'mosquitto_pub -h localhost -t "rustuya/command" -m '{
"action": "remove",
"id": "device_id"
}'mosquitto_pub -h localhost -t "rustuya/command" -m '{
"action": "set",
"id": "device_id",
"dps": {"1": true, "2": 50}
}'mosquitto_pub -h localhost -t "rustuya/command" -m '{"action": "status"}'mosquitto_pub -h localhost -t "rustuya/command" -m '{"action": "clear"}'MQTT topics and payloads can be fully customized using templates.
- Topic Template (
--mqtt-event-topic): Used for device status updates. - Message Topic (
--mqtt-message-topic): Used for errors and responses.
Variables:
{root}: MQTT root topic prefix{id}: Device ID (orbridgefor bridge-level responses){name}: Device Name (orbridgefor bridge-level responses){cid}: Sub-device CID (if applicable, otherwise empty){type}:activeorpassive(only for event topic){dp}: Data Point ID (only in single DP mode){value}: Data Point Value (only in single DP mode){dps}: JSON string of all Data Points{timestamp}: Unix timestamp{level}:responseorerror(only for message topic)
If --mqtt-command-topic contains variables like {id}, the bridge will automatically extract them from the incoming topic.
Example:
- Command Topic:
tuya/command/{id}/set - Received on:
tuya/command/light_1/setwith payloadtrue - Action: Sets the device
light_1totrue(if{dp}is also in the topic, it sets that specific DP).
- If
--mqtt-payload-templatecontains{value}or{dp}, the bridge will publish a separate message for each changed DP. - Otherwise, it will publish a single message containing all changed DPs in
{dps}.
| Action | Parameters | Description |
|---|---|---|
add |
id, key?, ip?, version?, name?, cid?, parent_id? |
Register a device. key (alias: local_key) required for direct devices. |
remove |
id or name |
Unregister device(s). Alias: delete. Supports lists [...]. |
clear |
- | Remove all devices. |
status |
- | Get bridge and device status. Alias: query. |
get |
id or name, cid? |
Query device status. Supports lists [...]. |
set |
id or name, dps, cid? |
Set device state. Supports lists [...]. |
request |
id or name, cmd, data?, cid? |
Send raw command. Supports lists [...]. |
sub_discover |
id or name |
Trigger sub-device discovery (Gateways). |
scan |
- | Scan for local devices (UDP 6666/6667). |
- ID Priority: If both
idandnameare provided,idis used andnameis ignored. - Lists Support: Both
idandnamecan be a single string or a list of strings.- Example:
"id": ["dev1", "dev2"] - Example:
"name": ["kitchen", "living_room"]
- Example:
Note: Add
"cid": "SUB_DEVICE_ID"toget,set, orrequestfor sub-device control.
The bridge publishes events to the following MQTT topics:
mqtt-event-topic: Device status changes (Active/Passive).mqtt-message-topic: Errors and logs.mqtt-scanner-topic: Results from thescanaction. Returns an empty object{}when a scan cycle is finished.{root}/bridge/config: Retained snapshot of the running configuration, published at startup and cleared on graceful shutdown (also serves as the presence/heartbeat topic).
Running two bridges against the same MQTT broker + root topic is prevented.
Device registrations are persisted to --state-file (default rustuya.json)
with debounced writes. The path is treated as relative to the working
directory — use an absolute path (or the Docker default /data/rustuya.json)
when running under systemd.
If
--mqtt-retainis enabled, do not delete the state file to "reset" the bridge. The retain-scavenger only runs in response toremove/clearactions while the bridge is alive, so deleting the file leaves stale retained messages on the broker for devices it no longer knows about. Send aclearcommand (or per-deviceremove) first. With the defaultmqtt_retain = false, deleting the file is harmless.
docs/internals.md — deep dive on internals: device
lifecycle, unified listener, template engine, retain scavenger, sub-device
routing, MQTT reconnection, state persistence, and operator tips. For
advanced users only; not needed to use the bridge.