Private
Public Access
Codacy Security Scan / Codacy static analysis (push) Has been cancelled
Codacy Coverage / Tests + coverage (push) Has been cancelled
gitleaks / scan (push) Has been cancelled
Lint / ruff (Python 3.14) (push) Has been cancelled
199 lines
8.4 KiB
Markdown
199 lines
8.4 KiB
Markdown
# 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](https://github.com/jasonacox/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](https://docs.astral.sh/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
|
|
|
|
```bash
|
|
git clone https://github.com/swackhamer/tuya-re.git
|
|
cd tuya-re
|
|
uv sync
|
|
```
|
|
|
|
## Configure the project
|
|
|
|
1. Copy the template:
|
|
|
|
```bash
|
|
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](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
|
|
|
|
```bash
|
|
uv run fingerbot devices # list configured names and the default
|
|
uv run fingerbot scan # find the device, print its address
|
|
uv run fingerbot status # connect, pair, dump datapoints
|
|
uv run fingerbot click # press the default named device
|
|
uv run fingerbot --device desktop click
|
|
uv run fingerbot press --duration 3 # press and hold for 3 seconds
|
|
uv run fingerbot --device all click --repeat 10 # all 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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.
|