No description
  • Python 98.5%
  • Makefile 1.1%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-16 22:24:44 +08:00
doc update 2026-09-16 22:24:44 +08:00
tests update 2026-09-16 22:24:44 +08:00
tuyactl update 2026-09-16 22:24:44 +08:00
.gitignore first commit 2026-09-16 17:40:56 +08:00
example.py first commit 2026-09-16 17:40:56 +08:00
Makefile first commit 2026-09-16 17:40:56 +08:00
README.md update 2026-09-16 22:24:44 +08:00
run-tests.sh first commit 2026-09-16 17:40:56 +08:00
tuya first commit 2026-09-16 17:40:56 +08:00

tuyactl

Control Tuya and Smart Life devices from the command line or from Python, directly over your WiFi or through the Tuya Cloud API.

tuya list
tuya on "WiFi Plug"
tuya brightness bedroom 40
tuya color bedroom warm

It uses only the Python 3 standard library, so you don't need to pip install anything or set up a virtualenv.

LAN Cloud
Typical command time 0.2–0.4 s 1–2 s
Works without internet Yes No
Works from anywhere No, same network only Yes
Needs the IoT Core subscription Only for tuya local setup Always

Contents

Install

make install                      # installs to ~/.local, no root needed
Installed file Location
tuya command ~/.local/bin/tuya
Python package ~/.local/lib/tuyactl/
Manual page ~/.local/share/man/man1/tuya.1

To install somewhere else, set PREFIX. DESTDIR is supported for packaging.

sudo make install PREFIX=/usr/local
make uninstall                    # use the PREFIX you installed with

Install and uninstall never touch your settings in ~/.config/tuyactl.

If man tuya doesn't find the page after installing, make install prints the line to add to your shell profile:

export MANPATH="$HOME/.local/share/man:$MANPATH"

You can also run ./tuya from the repository without installing.

Setup

You need a free Tuya IoT Platform cloud project to get API credentials.

  1. Create an account on the Tuya IoT Platform.
  2. Go to Cloud → Development and create a cloud project. Choose the data centre where your phone app account is registered.
  3. Copy the Access ID and Access Secret from the project overview.
  4. On the project's Devices tab, choose Link App Account and scan the QR code with the Smart Life or Tuya app. This step is what makes your devices visible to the API.
  5. Make sure the IoT Core API service is authorised for the project.

Then run:

tuya init

tuya init asks for the credentials and region, checks them with Tuya, lists the devices it finds and saves the settings. To control devices over your WiFi as well, continue with LAN control.

Pick the right region. It must match where your app account was registered, which isn't necessarily where you live. If init says the Access ID is unknown, run it again and try another region.

Region Data centre
us Western America
us-e Eastern America
eu Central Europe
eu-w Western Europe
cn China
in India
sg Singapore

LAN control

Once your devices are set up for it, tuya sends commands straight to them over your network. A one-time setup uses the cloud to fetch each device's local key:

tuya local setup
NAME         ADDRESS        VERSION  RESULT
WiFi Plug 2  192.168.0.190  3.3      ok
WiFi Plug    192.168.0.195  3.3      ok

tuya local setup does three things:

  1. It fetches each device's local key and data point numbers from the cloud.
  2. It listens on the network (for up to 20 seconds) to learn each device's address and protocol version.
  3. It tests each device with a status query.

From then on, commands to those devices go over the LAN:

tuya on "WiFi Plug"      # WiFi Plug: ON  (via lan 192.168.0.195 v3.3)

Modes

Mode Behaviour
auto (default) LAN for devices set up for it, cloud for the rest. If a LAN command fails, it is retried through the cloud with a note. Devices set up for the LAN keep working when the internet is down.
local LAN only. The cloud is never contacted, and no cloud credentials are needed.
cloud Cloud only.
tuya mode                     # show the mode
tuya mode local               # set it
tuya --cloud status desk      # override it for one command
tuya --local on desk

In local mode, tuya list checks each device directly, taking up to two seconds in total, and shows whether it is online and switched on:

NAME   STATE    POWER  VIA                 TYPE  ID
Plug2  online   off    lan 192.168.0.190   cz    a30482f1bdedda1d40vugr
Plug1  online   on     lan 192.168.0.195   cz    a336e5e49e6e1437f54e3q

In the other modes, the online state comes from the cloud's device list. If the cloud can't be reached, tuya list checks LAN devices directly, as in local mode.

Managing LAN devices

tuya local show                             # addresses and versions
tuya local show --keys                      # include the local keys
tuya local scan                             # find devices, update addresses
tuya local set desk --ip 192.168.0.190      # set details by hand
tuya local set desk --version 3.4
tuya local remove desk                      # go back to the cloud

tuya local setup and tuya local scan listen for the announcements devices broadcast every 5–20 seconds. Some broadcasts get lost over WiFi, so if a device is missed, run tuya local scan or tuya local set --ip.

Keeping LAN control working

  • Same network: the computer must be on the same network as the devices.
  • Fixed addresses: give each device a fixed address (a DHCP reservation in your router). If an address changes, tuya local scan picks up the new one.
  • Re-pairing: removing a device from the app and adding it again gives it a new local key, so run tuya local setup again afterwards.
  • Protocol versions: 3.1 to 3.5 are supported. Some 3.5 devices only announce themselves when asked, so set their address by hand.
  • Hubs: devices behind a hub or gateway (such as Zigbee devices) aren't supported over the LAN.
  • Connection limits: some devices accept only one or a few LAN connections at once, so other software holding connections open (such as Home Assistant) can get in the way.
  • Key security: local keys give full control of a device. They're stored in the config file, which only you can read.

Data points on the LAN

Over the LAN, devices number their data points instead of naming them. tuya translates using the numbers saved at setup, so tuya set plug switch_1 off works on both paths. tuya spec shows the numbers (#1), and tuya set also accepts a number directly, such as tuya set plug 1 off.

tuya status shows data points missing from the device's published specification by number. Colour values and a plug's power-on behaviour (relay_status) are converted between the LAN and cloud formats. Other enumerated values are sent as given, and a few devices use different names on the LAN.

Commands

Command What it does
tuya init [--region R] Set up or change credentials
tuya list [--refresh] List devices (alias: ls)
tuya status DEVICE Show the on/off state and every data point
tuya on DEVICE / off DEVICE Switch a device on or off
tuya toggle DEVICE Switch a device to the opposite state
tuya brightness DEVICE 0-100 Set brightness (alias: dim)
tuya temp DEVICE 0-100 Set white colour temperature (0 = warm, 100 = cool)
tuya color DEVICE COLOUR Set an RGB colour (alias: colour)
tuya set DEVICE CODE VALUE Write any data point
tuya spec DEVICE Show what a device supports
tuya alias [NAME [DEVICE]] [-d] List, add or delete short names
tuya mode [auto|local|cloud] Show or set how devices are reached
tuya local setup Prepare devices for LAN control
tuya local scan Find devices on the network
tuya local show [--keys] Show saved LAN details
tuya local set DEVICE [--ip] [--version] [--key] Set LAN details by hand
tuya local remove DEVICE Stop using the LAN for a device
tuya refresh Fetch the device list again
tuya raw GET|POST PATH [BODY] Call any Tuya API path

The full reference, including exit codes, is in man tuya. The same page is at doc/tuya.1; view it with make man.

Naming devices

A DEVICE argument can be any of these:

  • an alias
  • an exact name (not case-sensitive)
  • part of a name
  • a full device ID
  • the start of a device ID

If a name matches more than one device, tuya lists the matches and does nothing. Put quotes around names that contain spaces.

tuya alias desk "WiFi Plug 2"
tuya off desk
tuya alias desk --delete

Colours

tuya color accepts:

  • hex: #ff8800, ff8800 or #f80
  • decimal RGB: 255,136,0
  • a name: red, green, blue, white, warm, cool, yellow, orange, purple, pink, cyan or magenta

brightness, temp and color also switch the device on and put it in the right mode (white or colour).

Controls without their own command

Each Tuya device exposes its controls as data points, and vendors choose their own codes for them. tuya spec lists the codes a device accepts, and tuya set writes any of them:

tuya spec "WiFi Plug"
tuya set plug switch_1 off
tuya set fan fan_speed_enum 3
tuya set lamp colour_data_v2 '{"h":240,"s":1000,"v":800}'

tuya set converts the value before sending it:

  • on, off, true, false, yes and no become booleans.
  • Numbers become numbers.
  • Valid JSON starting with { or [ is sent as JSON.
  • Anything else is sent as a string.

Scripting

--json prints machine-readable output for list, status, spec, local scan and local show. Put it before the command:

tuya --json status desk | jq .status
tuya --json list | jq -r '.[] | select(.online) | .name'

Exit status is 0 on success, 1 for errors (a Tuya error, an unknown device or invalid input) and 2 for missing credentials or bad usage.

Python library

from tuyactl import connect

home = connect()                    # uses the same config and mode as the CLI

for device in home:
    print(device.name, device.online, device.is_on(), device.capabilities())

lamp = home["bedroom"]              # same name matching as the CLI
lamp.on()
lamp.set_brightness(40)
lamp.set_color("#ff8800")
lamp.set_color_temp(80)
lamp.set("work_mode", "scene")      # any data point
print(lamp.status(refresh=True))    # {code: value}
print(lamp.get_brightness())        # percent

home.all_off()                      # every device that has a switch
home.close()                        # close LAN connections

connect(mode="local") or connect(mode="cloud") overrides the saved mode. Home also works as a context manager (with connect() as home:). After a call, device.via says how the device was reached.

You can also pass credentials directly, which always uses the cloud:

home = connect(client_id="...", secret="...", region="eu")

All errors are subclasses of tuyactl.TuyaError:

Exception Raised when
ConfigError Credentials are missing or the config file is corrupt
AuthError The region is invalid
ApiError Tuya rejects a request (.code holds Tuya's error number)
DeviceNotFound No device matches the name, or the device lacks the capability
AmbiguousDevice The name matches more than one device
tuyactl.local.LocalConnectionError A device can't be reached over the LAN
tuyactl.local.LocalProtocolError A device's reply can't be decoded (usually a wrong local key)

example.py is a short runnable example.

Configuration

Path Contents
~/.config/tuyactl/config.json Access ID, secret, region, aliases, mode and LAN details (mode 600)
~/.config/tuyactl/cache.json Access token and device list (safe to delete)

The device list is cached for six hours. Device state is always read live.

Two optional settings can be added to config.json by hand:

  • local_timeout (top level): how many seconds to wait for a device. The default is 5.
  • port (in a device's entry under local): the device's TCP port. The default is 6668.

Environment variables override the config file:

Variable Purpose
TUYA_CLIENT_ID, TUYA_SECRET Credentials; with both set, no config file is needed
TUYA_REGION Region
TUYA_HOST, TUYA_SCHEME Send cloud requests to another server, such as a test mock
TUYACTL_UDP_PORTS Comma-separated ports to listen on for devices (for testing)
TUYACTL_PURE_AES Always use the built-in AES, even if cryptography is installed
XDG_CONFIG_HOME Base directory for configuration (default ~/.config)

API expiry

  • Access ID and secret: these don't expire.
  • Access token: this lasts about two hours. tuya renews it automatically.
  • IoT Core subscription: this does expire. New projects start on a free trial. You can extend it at no cost in the Tuya IoT console under Cloud → Cloud Services → IoT Core. When it lapses, cloud requests fail until you renew it. Your devices keep working in the phone app, and devices set up for LAN control keep working in tuya. You only need the subscription again to run tuya local setup, for example after re-pairing a device. The service page shows your expiry date, so note it somewhere.

Troubleshooting

Error Likely cause
[1004] sign invalid Wrong Access Secret, or your system clock is off
[1005] / unknown client_id Wrong region; run tuya init again
[1106] permission deny IoT Core isn't authorised, or the app account isn't linked
[2001] or an empty tuya list No app account is linked to the project
[28841101] The IoT Core subscription has expired
No device matches ... The device was added or renamed; run tuya refresh
... has no brightness control The device doesn't support it; check tuya spec
note: LAN control of ... failed The device isn't reachable on the LAN, so the cloud was used; check the address with tuya local scan
could not decrypt reply / closed the connection Wrong local key or protocol version; run tuya local setup
not found on this network The device wasn't heard during setup; run tuya local scan --timeout 40 or tuya local set --ip
cannot listen for devices Another program holds UDP ports 6666/6667/7000; set addresses with tuya local set --ip

Development

make test       # run the test suite
make man        # preview the manual page
make clean      # remove bytecode caches

The tests need no credentials and never touch real devices. They use two kinds of mock:

  • Cloud: tests/mock_tuya.py checks request signatures with its own HMAC code, separate from the client's.
  • Devices: tests/mock_device.py plays the device side of protocols 3.1 to 3.5.

Some tests also check against outside references:

  • AES: the built-in AES is tested against the FIPS-197 and GCM specification test vectors.

  • tinytuya: if the tinytuya library is installed, the tests also check that it can talk to the mock devices and that the frame formats match. To run those:

    python3 -m venv .venv && .venv/bin/pip install tinytuya
    make test PYTHON=.venv/bin/python
    
File Role
tuyactl/api.py Cloud request signing, token handling, HTTP
tuyactl/local.py LAN protocol (3.1–3.5), discovery, value conversion
tuyactl/crypto.py AES-ECB and AES-GCM in pure Python
tuyactl/backends.py Cloud, LAN and automatic routing to devices
tuyactl/devices.py Device model, capability detection, value scaling, name matching
tuyactl/config.py Config file and cache
tuyactl/cli.py Command-line interface
tuyactl/errors.py Exceptions and error-code hints
doc/tuya.1 Manual page

tuya reads each device's published specification to choose which codes to send. That's why on() works both on a bulb that uses switch_led and on a plug that uses switch_1. Brightness and colour temperature are scaled to the range each device reports, not to a fixed range.