d3eb5d286a
Made-with: Cursor
129 lines
4.8 KiB
Python
129 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Push state/commands to a WLED device.
|
|
Accepts a JSON file or simple flags (on, off, bri, preset).
|
|
Uses only standard library (no pip install).
|
|
"""
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from typing import Optional
|
|
from urllib.request import Request, urlopen
|
|
from urllib.error import URLError, HTTPError
|
|
|
|
DEFAULT_HOST = "192.168.240.30"
|
|
|
|
|
|
def push(host: str, payload: dict, return_state: bool = False) -> Optional[dict]:
|
|
base = host if host.startswith("http") else f"http://{host}"
|
|
url = f"{base}/json/state"
|
|
|
|
if return_state:
|
|
payload["v"] = True
|
|
|
|
body = json.dumps(payload).encode("utf-8")
|
|
req = Request(
|
|
url,
|
|
data=body,
|
|
method="POST",
|
|
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
)
|
|
try:
|
|
with urlopen(req, timeout=10) as r:
|
|
raw = r.read().decode()
|
|
if not raw:
|
|
return None
|
|
return json.loads(raw)
|
|
except HTTPError as e:
|
|
print(f"HTTP error: {e.code} {e.reason}", file=sys.stderr)
|
|
if e.fp:
|
|
print(e.fp.read().decode(), file=sys.stderr)
|
|
sys.exit(1)
|
|
except URLError as e:
|
|
print(f"Request failed: {e.reason}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except json.JSONDecodeError as e:
|
|
print(f"Invalid response JSON: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description="Push state/commands to WLED")
|
|
p.add_argument("--host", "-H", default=DEFAULT_HOST, help=f"WLED host (default: {DEFAULT_HOST})")
|
|
p.add_argument("--file", "-f", metavar="JSON", help="JSON file with state to send")
|
|
p.add_argument("--on", action="store_true", help="Turn on")
|
|
p.add_argument("--off", action="store_true", help="Turn off")
|
|
p.add_argument("--toggle", "-t", action="store_true", dest="toggle", help="Toggle on/off")
|
|
p.add_argument("--bri", type=int, metavar="0-255", help="Global brightness")
|
|
p.add_argument("--seg-bri", type=int, metavar="0-255", dest="seg_bri", help="Segment 0 brightness")
|
|
p.add_argument("--white", "-w", type=int, metavar="0-255", help="Segment 0 white channel (col[0][3], RGBW)")
|
|
p.add_argument("--cct", type=int, metavar="0-255", help="Color temp: 0=warm, 255=cool white (segment 0)")
|
|
p.add_argument("--preset", "-p", type=int, metavar="ID", help="Load preset by ID")
|
|
p.add_argument("--v", action="store_true", help="Return full state in response")
|
|
args = p.parse_args()
|
|
|
|
if args.file:
|
|
with open(args.file) as f:
|
|
payload = json.load(f)
|
|
else:
|
|
payload = {}
|
|
|
|
if args.off:
|
|
payload["on"] = False
|
|
if args.on:
|
|
payload["on"] = True
|
|
if args.toggle:
|
|
payload["on"] = "t"
|
|
if args.bri is not None:
|
|
payload["bri"] = max(0, min(255, args.bri))
|
|
if args.seg_bri is not None:
|
|
seg_bri = max(0, min(255, args.seg_bri))
|
|
payload.setdefault("seg", [])
|
|
seg0 = next((s for s in payload["seg"] if s.get("id") == 0), payload["seg"][0] if payload["seg"] else None)
|
|
if seg0 is None:
|
|
payload["seg"].insert(0, {"id": 0, "bri": seg_bri})
|
|
else:
|
|
seg0["bri"] = seg_bri
|
|
if args.white is not None:
|
|
w = max(0, min(255, args.white))
|
|
payload.setdefault("seg", [])
|
|
seg0 = next((s for s in payload["seg"] if s.get("id") == 0), payload["seg"][0] if payload["seg"] else None)
|
|
if seg0 is None:
|
|
payload["seg"].insert(0, {"id": 0, "fx": 0, "col": [[0, 0, 0, w], [0, 0, 0, 0], [0, 0, 0, 0]]})
|
|
else:
|
|
seg0.setdefault("col", [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
|
|
if len(seg0["col"]) < 1:
|
|
seg0["col"] = [[0, 0, 0, w], [0, 0, 0, 0], [0, 0, 0, 0]]
|
|
else:
|
|
prim = list(seg0["col"][0])
|
|
while len(prim) < 4:
|
|
prim.append(0)
|
|
prim[3] = w
|
|
seg0["col"][0] = prim
|
|
seg0["fx"] = 0 # Solid so white channel is visible
|
|
if args.cct is not None:
|
|
cct = max(0, min(255, args.cct))
|
|
payload.setdefault("seg", [])
|
|
seg0 = next((s for s in payload["seg"] if s.get("id") == 0), payload["seg"][0] if payload["seg"] else None)
|
|
if seg0 is None:
|
|
payload["seg"].insert(0, {"id": 0, "fx": 0, "cct": cct})
|
|
else:
|
|
seg0["cct"] = cct
|
|
seg0["fx"] = 0 # Solid so CCT is visible
|
|
if args.preset is not None:
|
|
payload["ps"] = args.preset
|
|
if args.v:
|
|
payload["v"] = True
|
|
|
|
if not payload:
|
|
print("Nothing to send. Use --file, --on, --off, --toggle, --bri, or --preset.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
out = push(args.host, payload, return_state=args.v)
|
|
if out:
|
|
print(json.dumps(out, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|