Control your Tuya devices locally — no cloud, no phone app, and (for the direct paths) no gateway. tuya-re is a from-scratch implementation of Tuya's local protocols: after a one-time credential pull, your commands never leave your machine or your LAN.
The project merges three earlier proofs of concept into one tool. It drives four device paths:
| Device family | Transport | Command group |
|---|---|---|
| Fingerbots | Bluetooth LE (Tuya BLE v3) | ble |
| Wi-Fi switches and plugs | LAN (Tuya local v3.3 and v3.4) | switch |
| IR blasters (GWW-RC03 / JMIR05) | Bluetooth LE (Tuya BLE v3) | ir |
| Fingerbots behind a Tuya BLE gateway | LAN, via tinytuya | gateway |
The BLE and LAN stacks (framing, crypto, sessions) are hand-written on top of
bleak and pycryptodome. Only the gateway path delegates protocol work to
tinytuya.
- uv — all commands run through it.
- Python 3.11 or later (development and CI use 3.14; uv installs it for you).
- For BLE commands on macOS: run from Terminal or iTerm and approve the Bluetooth permission prompt on first use. A process launched without a GUI parent is stopped by the OS instead of prompted.
git clone https://github.com/swackhamer/tuya-re.git
cd tuya-re
uv sync-
Copy the template:
cp config.example.toml config.toml
-
Get your device credentials. At https://iot.tuya.com, create a cloud project, link your Smart Life app account (Devices > Link App Account, then scan the QR code from the app), and open Cloud > API Explorer > Query Device Details (
GET /v1.0/devices/{id}). The response containslocal_key,uuid, andproduct_id. -
Fill in
config.toml. Configure only the sections you need — each maps to one command group.config.tomlis git-ignored and is the only place real credentials belong. For what's secret and how to rotate keys, see SECURITY.md.
Notes:
- The cloud response's
ipis your public WAN address, not the device's LAN address. Leaveipunset and let UDP discovery find the real one. - The
local_keyrotates when a device is re-paired in the app. If control starts failing, re-pull the key and updateconfig.toml. - For the gateway path, the gateway's
local_keyand the Fingerbot'snode_idcome fromuv run python -m tinytuya wizard(delete its output files after copying the values).
Global flags (-v, --device, --timeout, --config) go before the
command. --device selects a named device, all for every configured device
of that type, or omit it for the configured default.
Every command group is available under the unified entry point,
uv run tuya-re <group> <command>. The standalone fingerbot, switch, and
ir scripts are shortcuts that skip the group prefix.
uv run fingerbot scan # find the device, print its address
uv run fingerbot status # connect, pair, dump datapoints
uv run fingerbot click # one press
uv run fingerbot press --duration 3 # press and hold about 3 seconds
uv run fingerbot --device all click --repeat 10 # both robots, in parallelThe tool scans for the Tuya BLE service, matches the uuid decrypted from the
advertisement, runs the handshake (DEVICE_INFO, derive the AES session key
from srand, then PAIR), and writes the click datapoint with a guaranteed
rising edge. --repeat and --interval fire multiple clicks over one
connection; the arm needs about 1.5 seconds per press-and-release cycle.
uv run switch status # discover, connect, report on or off
uv run switch on
uv run switch off
uv run switch toggle
uv run switch --device all off # every switch, concurrentlyswitch finds the device by UDP broadcast (ports 6666/6667), then opens an
encrypted TCP session on port 6668 keyed by local_key. Protocol v3.3 uses
AES-ECB framed messages; v3.4 negotiates a session key first, then uses
HMAC-SHA256-framed messages. The relay is DP 1. (v3.5 is detected but not
implemented.)
uv run ir scan # find the blaster(s)
uv run ir status # connect, pair, dump datapoints
uv run ir learn # study mode: capture a code from a remote
uv run ir send "8iOQERoC" # replay a captured codelearn puts the blaster in study mode; point the original remote at it and
press a button. The captured code prints as base64 — Tuya's format of
little-endian 16-bit microsecond pulse durations — and send replays it.
DP defaults (send 201 as string, learn/study 202) are confirmed for the
JMIR05 module; override them with --send-dp, --send-dp-type,
--learn-dp, and --study-dp if your model differs. Unlike the global
flags, these four go after the learn or send command — for example,
uv run ir send --send-dp 105 "8iOQERoC".
uv run tuya-re gateway status # dump sub-device status via the gateway
uv run tuya-re gateway click # set click mode, then pressThis path keeps Tuya's gateway in the loop: the Fingerbot is addressed as a
sub-device (cid = the wizard's node_id) of the gateway, and tinytuya
handles the LAN protocol. Status reads on BLE sub-devices often fail through
the gateway even though writes work (tinytuya issue #677) — the click command
prints the status but doesn't stop on it. If the handshake fails, try
version = "3.4" first, then "3.5", then "3.3".
crypto,packet,protocol— Tuya BLE v3: CRC16, MD5-derived login/session keys, AES-CBC frames, GATT chunking and reassembly.ble— scanning, advertisement uuid decryption, connected sessions.devices— per-product Fingerbot datapoint maps.lan_protocol,lan_session,lan_discovery— Tuya local v3.3/v3.4:0x000055AAframing, session-key negotiation, UDP discovery.ir— IR pulse codec and the learn/send flow.gateway— tinytuya sub-device addressing for the gateway path.config,cli— TOML credential loading and the argparse front end.
The protocol layers are transport-agnostic and unit-tested against fakes that
do real crypto, with framing pinned to byte-exact golden vectors from
tinytuya and ha_tuya_ble.
uv run pytest # run the test suite
uv run pytest --cov # with coverage
uv run ruff check src tests # lint
uv run ruff format --check src tests # formattingble.py, lan_discovery.py, and cli.py are hardware and entry-point
boundaries, exercised against real devices rather than unit tests; they're
excluded from coverage.
Four GitHub Actions workflows run on every push and pull request to main:
- Lint — ruff lint and format checks on Python 3.14.
- Codacy Coverage — runs the tests with coverage and uploads the report
to Codacy. Until the
CODACY_PROJECT_TOKENrepository secret is set, the job still runs the tests and passes; it just skips the upload. - Codacy Security Scan — static analysis; results appear under Security > Code scanning.
- gitleaks — scans the full git history for leaked secrets on every push.
| Symptom | Fix |
|---|---|
device ... not seen on UDP (switch) |
Make sure the plug is powered and on the same LAN, or set a static ip in config.toml. |
| Switch set fails or decrypt error | The local_key is stale — re-pull it from the API Explorer. |
device not found (N Tuya devices seen) (BLE) |
BLE advertising is intermittent; retry or raise --timeout. Power off the gateway and close the phone app — BLE allows one central. |
| BLE command stops immediately on macOS | Bluetooth permission — run from a real terminal and approve the prompt. |
Gateway handshake fails or Error 914 |
Try version = "3.4", then "3.5", then "3.3". |
| Gateway status shows an error but clicks work | Expected for BLE sub-devices (tinytuya issue #677) — trust the write result. |
This is an independent project for controlling devices you own. It isn't affiliated with or endorsed by Tuya.