My PC talks to my wifi light bulb directly, not via Tuya's cloud API.
I can say, "Alexa, bulb 1 red" or "Alexa bulb 1 off" etc.
I can also say, "Use @TRIGGERcmd to turn bulb 1 blue" using the MCP tool with ChatGPT.
Example commands:
Turn the bulb on:
python3 c:\tools\tuyabulb.py --id abcdefghijklmnop123456 --key "123456!@ABCcdefh" --ip 192.168.86.25 on
Set the color to red:
python3 c:\tools\tuyabulb.py --id abcdefghijklmnop123456 --key "123456!@ABCcdefh" --ip 192.168.86.25 red
Set the brightness to 60 percent:
python3 c:\tools\tuyabulb.py --id abcdefghijklmnop123456 --key "123456!@ABCcdefh" --ip 192.168.86.25 60
Here's the script:
#!/usr/bin/env python3
import argparse
import ipaddress
import platform
import subprocess
import sys
import tinytuya
# Predefined color presets (RGB values)
COLOR_PRESETS = {
"red": (255, 0, 0),
"green": (0, 255, 0),
"blue": (0, 0, 255),
"white": (255, 255, 255),
"yellow": (255, 255, 0),
"cyan": (0, 255, 255),
"magenta": (255, 0, 255),
"orange": (255, 165, 0),
"purple": (128, 0, 128),
"pink": (255, 192, 203),
}
def ping_host(ip: str, timeout: int = 1) -> bool:
"""Ping a host to check if it's reachable. Returns True if ping succeeds."""
param = "-n" if platform.system().lower() == "windows" else "-c"
timeout_param = "-w" if platform.system().lower() == "windows" else "-W"
timeout_value = str(timeout * 1000) if platform.system().lower() == "windows" else str(timeout)
command = ["ping", param, "1", timeout_param, timeout_value, ip]
try:
result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout + 1)
return result.returncode == 0
except (subprocess.TimeoutExpired, Exception):
return False
def make_device(device_id: str, ip: str, local_key: str, version: str | None):
# BulbDevice works for most Tuya bulbs. If yours isn't a "bulb" type, use Device instead.
d = tinytuya.BulbDevice(device_id, ip, local_key)
d.set_socketPersistent(True) # keep socket open for reliability
d.set_socketTimeout(5) # seconds
# Many Tuya WiFi bulbs are 3.3. Some are 3.1. If you're unsure, try 3.3 first.
if version:
d.set_version(float(version))
else:
d.set_version(3.3)
return d
def main():
p = argparse.ArgumentParser(description="Local control of a Tuya bulb via tinytuya (no cloud).")
p.add_argument("--id", required=True, help="Tuya device id")
p.add_argument("--ip", help="Bulb IP address on your LAN")
p.add_argument("--subnet", help="Subnet to scan (e.g. 192.168.1.0/24)")
p.add_argument("--start-from", help="IP address to start scanning from (only with --subnet)")
p.add_argument("--key", required=True, help="Tuya localKey (16+ chars)")
p.add_argument("--ver", default=None, help="Protocol version (e.g. 3.3 or 3.1). Default: 3.3")
p.add_argument("cmd", help="Command: off, on, status, color name (red, green, etc.), or brightness 0-100")
args = p.parse_args()
# Determine command type
cmd_lower = args.cmd.lower()
if cmd_lower in ["off", "on", "status"]:
cmd_type = cmd_lower
elif cmd_lower in COLOR_PRESETS:
cmd_type = "color"
preset_color = cmd_lower
elif args.cmd.isdigit():
brightness_val = int(args.cmd)
if 0 <= brightness_val <= 100:
cmd_type = "brightness"
else:
p.error("Brightness must be between 0-100")
else:
p.error(f"Invalid command: {args.cmd}. Use: off, on, status, color name, or brightness (0-100)")
# Validate that either --ip or --subnet is provided
if not args.ip and not args.subnet:
p.error("Either --ip or --subnet must be specified")
if args.ip and args.subnet:
p.error("Cannot specify both --ip and --subnet")
if args.start_from and not args.subnet:
p.error("--start-from can only be used with --subnet")
# Determine which IPs to try
if args.subnet:
try:
network = ipaddress.ip_network(args.subnet, strict=False)
all_ips = [str(ip) for ip in network.hosts()]
# Filter to start from specified IP if provided
if args.start_from:
start_ip = ipaddress.ip_address(args.start_from)
# Verify start IP is in the subnet
if start_ip not in network:
print(f"ERROR: Start IP {args.start_from} is not in subnet {args.subnet}", file=sys.stderr)
return 1
# Filter to IPs >= start_from
ips_to_try = [ip for ip in all_ips if ipaddress.ip_address(ip) >= start_ip]
print(f"Scanning subnet {args.subnet} from {args.start_from} ({len(ips_to_try)} hosts)...", file=sys.stderr)
else:
ips_to_try = all_ips
print(f"Scanning subnet {args.subnet} ({len(ips_to_try)} hosts)...", file=sys.stderr)
except ValueError as e:
print(f"ERROR: Invalid subnet or IP format: {e}", file=sys.stderr)
return 1
else:
ips_to_try = [args.ip]
# Try each IP until one works
last_error = None
for ip in ips_to_try:
if args.subnet:
print(f"Trying {ip}...", file=sys.stderr)
# Quick ping check to skip unreachable hosts
if not ping_host(ip):
continue
dev = make_device(args.id, ip, args.key, args.ver)
dev = make_device(args.id, ip, args.key, args.ver)
try:
if cmd_type == "off":
r = dev.turn_off()
elif cmd_type == "on":
r = dev.turn_on()
elif cmd_type == "brightness":
r = dev.set_brightness_percentage(brightness_val)
elif cmd_type == "color":
# Get RGB values from preset
rgb = COLOR_PRESETS[preset_color]
# Set the color
r = dev.set_colour(rgb[0], rgb[1], rgb[2])
else:
r = dev.status()
# Check if the device responded with an errora
if isinstance(r, dict) and r.get("Error"):
if args.subnet:
# During subnet scan, continue to next IP on error
last_error = r.get("Error")
continue
else:
# For direct IP, print error and exit
print(r)
return 2
# Success! Print result and exit
if args.subnet:
print(f"SUCCESS: Device found at {ip}", file=sys.stderr)
print(r)
return 0
except Exception as e:
last_error = e
if not args.subnet:
print(f"ERROR: {e}", file=sys.stderr)
return 1
# For subnet scan, continue to next IP
continue
finally:
try:
dev.set_socketPersistent(False)
except Exception:
pass
# If we get here with subnet scan, none of the IPs worked
if args.subnet:
print(f"ERROR: Device not found in subnet {args.subnet}. Last error: {last_error}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
This is my commands.json entry:
{
"trigger": "Tuya Bulb 1",
"command": "python3 c:\\tools\\tuyabulb.py --id abcdefghijklmnop123456 --key \"123456!@ABCcdefh\" --ip 192.168.86.25",
"offCommand": "",
"ground": "foreground",
"voice": "bulb 1",
"voiceReply": "",
"allowParams": "true",
"mcpToolDescription": "Controls the state of light bulb 1. Parameters are: on, off, red, green, blue, white, yellow, cyan, magenta, orange, purple, pink, or brightness percentage from 0 to 100"
}