#!/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()