Initial commit - Tasmota skill for OpenClaw

This commit is contained in:
2026-02-12 01:42:09 +00:00
commit d06cb6d0de
4 changed files with 575 additions and 0 deletions

101
scripts/tasmota-control.py Executable file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Tasmota Device Controller
Control Tasmota devices via HTTP JSON API
"""
import sys
import json
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
def tasmota_command(ip, command, timeout=5):
"""Send command to Tasmota device"""
try:
# Tasmota JSON API format: /cm?cmnd=COMMAND
cmd = command.replace(' ', '%20')
url = f"http://{ip}/cm?cmnd={cmd}"
req = Request(url, headers={'User-Agent': 'Tasmota-Controller'})
with urlopen(req, timeout=timeout) as response:
data = response.read()
return json.loads(data.decode('utf-8'))
except Exception as e:
return {"ERROR": str(e)}
def toggle_power(ip):
"""Toggle power on/off"""
return tasmota_command(ip, "Power")
def set_power(ip, state):
"""Set power state (ON/OFF/TOGGLE)"""
return tasmota_command(ip, f"Power {state.upper()}")
def get_status(ip, status_type="0"):
"""Get device status
status_type codes:
0 = Status
1 = StatusPRM (Parameters)
2 = StatusFWR (Firmware)
3 = StatusLOG (Log)
4 = StatusNET (Network)
5 = StatusMQT (MQTT)
9 = StatusTIM (Time)
"""
return tasmota_command(ip, f"Status {status_type}")
def set_brightness(ip, level):
"""Set brightness (0-100)"""
return tasmota_command(ip, f"Dimmer {level}")
def set_color(ip, color):
"""Set RGB color (hex or r,g,b)"""
return tasmota_command(ip, f"Color {color}")
def main():
if len(sys.argv) < 3:
print("Usage:")
print(" python3 tasmota-control.py <IP> power [on|off|toggle]")
print(" python3 tasmota-control.py <IP> status [0-9|all]")
print(" python3 tasmota-control.py <IP> brightness <0-100>")
print(" python3 tasmota-control.py <IP> color <hex>")
sys.exit(1)
ip = sys.argv[1]
cmd = sys.argv[2].lower()
if cmd == "power":
if len(sys.argv) > 3:
result = set_power(ip, sys.argv[3])
else:
result = toggle_power(ip)
elif cmd == "status":
if len(sys.argv) > 3:
status_type = sys.argv[3]
if status_type == "all":
# Get all statuses
result = {"statuses": {}}
for st in ["0", "1", "2", "3", "4", "5", "9"]:
result["statuses"][st] = get_status(ip, st)
else:
result = get_status(ip, status_type)
else:
result = get_status(ip, "0")
elif cmd == "brightness":
if len(sys.argv) < 4:
print("Error: brightness requires level (0-100)")
sys.exit(1)
result = set_brightness(ip, sys.argv[3])
elif cmd == "color":
if len(sys.argv) < 4:
print("Error: color requires hex value (e.g., FF0000)")
sys.exit(1)
result = set_color(ip, sys.argv[3])
else:
# Raw command
result = tasmota_command(ip, ' '.join(sys.argv[2:]))
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()

166
scripts/tasmota-discovery.py Executable file
View File

@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
Tasmota Device Discovery
Scans network for Tasmota devices by:
1. HTTP-based detection (port 80)
2. Checking for Tasmota JSON API response
3. Looking for Tasmota user-agent in responses
"""
import socket
import subprocess
import concurrent.futures
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
import json
import time
# Network configuration
SUBNET = "192.168.1.0/24"
TIMEOUT = 2 # seconds
def get_local_ip():
"""Get local IP to determine subnet"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "192.168.1.148"
def get_subnet_ips():
"""Generate list of IPs in subnet"""
local_ip = get_local_ip()
parts = local_ip.split('.')
base = f"{parts[0]}.{parts[1]}.{parts[2]}"
return [f"{base}.{i}" for i in range(1, 255)]
def check_http(ip):
"""Check if IP has HTTP service that might be Tasmota"""
try:
# Try basic HTTP request first
req = Request(f"http://{ip}/", headers={'User-Agent': 'Tasmota-Scanner'})
start = time.time()
with urlopen(req, timeout=TIMEOUT) as response:
elapsed = time.time() - start
data = response.read(5000).decode('utf-8', errors='ignore')
headers = dict(response.headers)
# Check for Tasmota indicators
is_tasmota = False
evidence = []
# Check for Tasmota-specific headers/content
if 'tasmota' in headers.get('Server', '').lower():
is_tasmota = True
evidence.append(f"Server header: {headers.get('Server')}")
# Check content for Tasmota patterns
if 'tasmota' in data.lower():
is_tasmota = True
evidence.append("Tasmota keyword in content")
# Check for known Tasmota page titles/elements
if 'Status' in data and 'tasmota' in data.lower():
is_tasmota = True
evidence.append("Tasmota status page detected")
# Try Tasmota JSON API
try:
req_api = Request(f"http://{ip}/cm?cmnd=status%200")
with urlopen(req_api, timeout=1) as api_response:
api_data = api_response.read(1000).decode('utf-8', errors='ignore')
if 'tasmota' in api_data.lower() or 'StatusSNS' in api_data or 'StatusNET' in api_data:
is_tasmota = True
evidence.append("Tasmota JSON API response")
except:
pass
if is_tasmota:
try:
title = data.split('<title>')[1].split('</title>')[0].strip()
except:
title = "N/A"
return {
'ip': ip,
'port': 80,
'title': title,
'server': headers.get('Server', 'Unknown'),
'evidence': evidence,
'elapsed': elapsed
}
return None
except (URLError, HTTPError, socket.timeout, ConnectionResetError):
return None
except Exception as e:
return None
def ping_scan(ip):
"""Quick ping check to see if host is up"""
try:
result = subprocess.run(
['ping', '-c', '1', '-W', '1', ip],
capture_output=True,
timeout=2
)
return result.returncode == 0
except:
return False
def discover_tasmota():
"""Main discovery function"""
print(f"🔍 Scanning network for Tasmota devices...")
print(f"📍 Local subnet: {SUBNET}")
ips = get_subnet_ips()
print(f"📡 Checking {len(ips)} IP addresses...")
tasmota_devices = []
# First, quick ping sweep to find live hosts
print("\n1⃣ Ping sweep (finding live hosts)...")
live_hosts = []
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
future_to_ip = {executor.submit(ping_scan, ip): ip for ip in ips}
for future in concurrent.futures.as_completed(future_to_ip):
ip = future_to_ip[future]
if future.result():
live_hosts.append(ip)
print(f"{ip}")
print(f"\n📍 Found {len(live_hosts)} live hosts")
print(f"\n2⃣ Scanning for Tasmota (HTTP) on live hosts...")
# Then check HTTP on live hosts
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
future_to_ip = {executor.submit(check_http, ip): ip for ip in live_hosts}
for future in concurrent.futures.as_completed(future_to_ip):
result = future.result()
if result:
tasmota_devices.append(result)
return tasmota_devices
if __name__ == "__main__":
devices = discover_tasmota()
print(f"\n{'='*60}")
if devices:
print(f"✅ Found {len(devices)} Tasmota device(s):\n")
for i, device in enumerate(devices, 1):
print(f"🔌 Device {i}:")
print(f" IP: {device['ip']}:{device['port']}")
print(f" Title: {device['title']}")
print(f" Server: {device['server']}")
print(f" Evidence:")
for ev in device['evidence']:
print(f"{ev}")
print(f" Response time: {device['elapsed']:.3f}s")
print()
else:
print("❌ No Tasmota devices found")
print(f"{'='*60}")

57
scripts/tasmota-status.py Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""
Get summary of all Tasmota devices from inventory
"""
import subprocess
import json
import time
from pathlib import Path
CSV_FILE = Path.home() / ".openclaw/workspace/memory/tasmota-inventory.csv"
def get_device_status(ip):
"""Get power status from device"""
try:
result = subprocess.run(
["python3", Path.home() / ".openclaw/workspace/scripts/tasmota-control.py", ip, "status", "0"],
capture_output=True,
timeout=3
)
if result.returncode == 0:
data = json.loads(result.stdout)
if "StatusSTS" in data:
return data["StatusSTS"].get("POWER", "UNKNOWN")
if "Status" in data:
return data["Status"].get("Power", "ON" if data["Status"].get("Power") == 1 else "OFF")
return "UNKNOWN"
except:
return "TIMEOUT"
print("🔍 Tasmota Device Summary")
print("="*80)
devices = []
with open(CSV_FILE) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(',')
if len(parts) >= 3 and parts[0].strip().startswith('192'):
ip = parts[0].strip()
name = parts[1].strip().replace(' - Main Menu', '')
version = parts[2].strip()
devices.append((ip, name, version))
# Quick check of first few devices
print(f"Found {len(devices)} devices\n")
print(f"{'IP Address':<18} {'Device Name':<30} {'Version':<12} {'Power'}")
print("-"*80)
for ip, name, version in devices:
power = get_device_status(ip)
print(f"{ip:<18} {name[:28]:<30} {version:<12} {power}")
time.sleep(0.1) # Don't overwhelm the network
print("="*80)