Skip to content

Repository files navigation

tuya-re

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.

Prerequisites

  • 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.

Install

git clone https://github.com/swackhamer/tuya-re.git
cd tuya-re
uv sync

Configure the project

  1. Copy the template:

    cp config.example.toml config.toml
  2. 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 contains local_key, uuid, and product_id.

  3. Fill in config.toml. Configure only the sections you need — each maps to one command group. config.toml is 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 ip is your public WAN address, not the device's LAN address. Leave ip unset and let UDP discovery find the real one.
  • The local_key rotates when a device is re-paired in the app. If control starts failing, re-pull the key and update config.toml.
  • For the gateway path, the gateway's local_key and the Fingerbot's node_id come from uv run python -m tinytuya wizard (delete its output files after copying the values).

Usage

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.

Fingerbot over Bluetooth

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 parallel

The 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.

Switch over the LAN

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, concurrently

switch 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.)

IR blaster over Bluetooth

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 code

learn 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".

Fingerbot through the Tuya gateway

uv run tuya-re gateway status        # dump sub-device status via the gateway
uv run tuya-re gateway click         # set click mode, then press

This 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".

How the pieces fit

  • 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: 0x000055AA framing, 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.

Develop

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 # formatting

ble.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.

Continuous integration

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_TOKEN repository 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.

Troubleshooting

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.

Disclaimer

This is an independent project for controlling devices you own. It isn't affiliated with or endorsed by Tuya.

About

Local control of Tuya devices - BLE Fingerbots, LAN switches, BLE IR blasters, gateway sub-devices. No cloud.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages