From e00d8be5e8aff7a6588bd3f932a6706574a07ee6 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sun, 18 Oct 2026 13:50:12 +0200 Subject: feat(ledstrip): switch the strip on and off with the session `udt-ledstrip on|off` switches the strip, replaying the gradient when it comes on; with no argument it still only replays the gradient, so a wallpaper change leaves a strip the user switched off alone. The last address is cached in ~/.cache/udt/ledstrip.json, because `off` runs as the session ends and cannot wait up to 20 s for a broadcast. From the cache a switch takes under half a second; discovery is the fallback when the cached address fails. hypr-theme calls it at login, on Hyprland's shutdown event, and around suspend from hypridle. Co-Authored-By: Claude Opus 5.5 --- bin/udt-ledstrip | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) (limited to 'bin/udt-ledstrip') diff --git a/bin/udt-ledstrip b/bin/udt-ledstrip index 0318bd8..ca39063 100755 --- a/bin/udt-ledstrip +++ b/bin/udt-ledstrip @@ -5,19 +5,22 @@ # Licensed under the GNU General Public License v2 only. """Run the Hyprland border's gradient along every Tuya addressable strip. -Usage: udt-ledstrip +Usage: udt-ledstrip play the gradient, leaving the strip on or off + udt-ledstrip on|off switch it, the gradient with it when on udt-ledstrip --selftest The colours are the three udt-accent wrote for the window border, the accent between its two hue neighbours, played as the strip's "flow" scene: a gradient moving along the strip. udt-accent starts this detached on every run, like -udt-keyboard. +udt-keyboard. hypr-theme's autostart runs `on` at login and `off` on +Hyprland's shutdown event, so the strip follows the session. The device id and local key come from ~/.config/udt/tuya/devices.json, written once by `python -m tinytuya wizard` against a Tuya IoT cloud project; after that everything is local. Re-pairing a light in the app changes its key, so -the wizard has to run again. The address is not stored: the light broadcasts -it on UDP 6667 every few seconds, so a DHCP change costs nothing. +the wizard has to run again. The address is cached in ~/.cache/udt, because +`off` runs as the session ends and cannot wait for discovery; when the cached +one fails, the light's own broadcast on UDP 6667 finds it again within seconds. Speaks Tuya protocol 3.5 itself (AES-GCM, session key negotiated per connection) with the `cryptography` module, rather than depending on tinytuya. @@ -38,6 +41,7 @@ from pathlib import Path DEVICES = Path.home() / ".config/udt/tuya/devices.json" BORDER = Path.home() / ".cache/udt/udt-border.lua" +ADDRESSES = Path.home() / ".cache/udt/ledstrip.json" # Every Tuya device encrypts its discovery broadcast with this published key. UDP_KEY = hashlib.md5(b"yGAdlopoPVldABfn").digest() @@ -153,7 +157,7 @@ def selftest(): def main(): if sys.argv[1:] == ["--selftest"]: return selftest() - if sys.argv[1:]: + if sys.argv[1:] not in ([], ["on"], ["off"]): sys.exit(__doc__.split("\n\n")[1]) try: devices = json.loads(DEVICES.read_text()) @@ -165,15 +169,40 @@ def main(): if d.get("mapping", {}).get("24", {}).get("code") == "colour_data"} border = BORDER.read_text() if BORDER.exists() else "" colours = ["#" + h for h in re.findall(r"rgb\(([0-9a-fA-F]{6})\)", border)] - if not colours or not lights: + if not lights: + return + if sys.argv[1:] == ["off"]: + dps = {"20": False} + elif colours: + dps = {"21": "scene", "51": scene(colours)} + if sys.argv[1:] == ["on"]: + dps["20"] = True + elif sys.argv[1:] == ["on"]: + dps = {"20": True} + else: return - dps = {"21": "scene", "51": scene(colours)} - for dev_id, ip in discover(set(lights)).items(): + try: + known = json.loads(ADDRESSES.read_text()) + except (OSError, ValueError): + known = {} + pending = set(lights) + for dev_id in pending & set(known): + try: + set_dps(known[dev_id], lights[dev_id], dps) + pending.discard(dev_id) + except Exception: + pass # moved, or unplugged; discovery below settles which + if not pending: + return + for dev_id, ip in discover(pending).items(): try: set_dps(ip, lights[dev_id], dps) + known[dev_id] = ip except Exception as e: print(f"udt-ledstrip: {dev_id} at {ip}: {e}", file=sys.stderr) + ADDRESSES.parent.mkdir(parents=True, exist_ok=True) + ADDRESSES.write_text(json.dumps(known)) if __name__ == "__main__": -- cgit v1.2.3