From 36f9d0a2847edf70694173a06c29b55d0ad788d0 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Thu, 2 Jul 2026 20:04:11 -0700 Subject: [PATCH] party-lock: add MQTT ACL-based plug lock service Listener watches party-lock//set and dynamically generates mosquitto ACL deny rules. Test against bamboo first, then dj-booth. Add lock button to CYD dashboard. --- infra/compose.yaml | 16 ++++++++ infra/mosquitto/config/acl.conf | 1 + infra/mosquitto/config/mosquitto.conf | 2 + infra/party-lock/Dockerfile | 6 +++ infra/party-lock/party-lock.py | 56 +++++++++++++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 infra/mosquitto/config/acl.conf create mode 100644 infra/party-lock/Dockerfile create mode 100644 infra/party-lock/party-lock.py diff --git a/infra/compose.yaml b/infra/compose.yaml index 4960e24..9021949 100644 --- a/infra/compose.yaml +++ b/infra/compose.yaml @@ -139,6 +139,22 @@ services: QOBUZ_USERNAME: "${QOBUZ_USERNAME:-}" QOBUZ_PASSWORD: "${QOBUZ_PASSWORD:-}" + # Party lock: blocks state changes to DJ Booth plug when locked. + # Watches party-lock/dj-booth/set and swaps mosquitto ACL file. + party-lock: + build: ./party-lock + container_name: party-lock + restart: unless-stopped + depends_on: + - mosquitto + volumes: + - ./mosquitto/config:/mosquitto/config + - /var/run/docker.sock:/var/run/docker.sock + environment: + MQTT_HOST: mosquitto + MQTT_PORT: "1883" + TZ: America/Los_Angeles + volumes: librespot_cache: diff --git a/infra/mosquitto/config/acl.conf b/infra/mosquitto/config/acl.conf new file mode 100644 index 0000000..af7ab2c --- /dev/null +++ b/infra/mosquitto/config/acl.conf @@ -0,0 +1 @@ +topic readwrite # diff --git a/infra/mosquitto/config/mosquitto.conf b/infra/mosquitto/config/mosquitto.conf index 8c1df4a..dea7748 100644 --- a/infra/mosquitto/config/mosquitto.conf +++ b/infra/mosquitto/config/mosquitto.conf @@ -1,6 +1,8 @@ listener 1883 allow_anonymous true +acl_file /mosquitto/config/acl.conf + persistence true persistence_location /mosquitto/data log_dest file /mosquitto/log/mosquitto.log diff --git a/infra/party-lock/Dockerfile b/infra/party-lock/Dockerfile new file mode 100644 index 0000000..b1d3a3e --- /dev/null +++ b/infra/party-lock/Dockerfile @@ -0,0 +1,6 @@ +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim + +COPY party-lock.py /app/party-lock.py +WORKDIR /app + +CMD ["uv", "run", "party-lock.py"] diff --git a/infra/party-lock/party-lock.py b/infra/party-lock/party-lock.py new file mode 100644 index 0000000..fc32add --- /dev/null +++ b/infra/party-lock/party-lock.py @@ -0,0 +1,56 @@ +#!/usr/bin/env -S uv run --script +# /// script +# dependencies = [ +# "paho-mqtt>=2.0", +# ] +# /// + +import os + +import paho.mqtt.client as mqtt + +MQTT_HOST = os.environ.get("MQTT_HOST", "mosquitto") +MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883")) +ACL_PATH = "/mosquitto/config/acl.conf" + +PLUG_MAP = { + "bamboo": "0xffffb40e06050af6", + "dj-booth": "0xffffb40e0603ef11", +} + +locked_plugs = set() + +def rebuild_acl(): + lines = [] + for plug in sorted(locked_plugs): + ieee = PLUG_MAP.get(plug) + if ieee: + lines.append(f"topic deny zigbee2mqtt/{ieee}/set") + lines.append("topic readwrite #") + with open(ACL_PATH, "w") as f: + f.write("\n".join(lines) + "\n") + os.system("docker kill -s HUP mosquitto 2>/dev/null || true") + +def on_message(client, userdata, msg): + topic = msg.topic + payload = msg.payload.decode().strip().upper() + parts = topic.split("/") + if len(parts) >= 3 and parts[0] == "party-lock": + plug_name = parts[1] + if plug_name in PLUG_MAP and payload in ("ON", "OFF"): + if payload == "ON": + locked_plugs.add(plug_name) + else: + locked_plugs.discard(plug_name) + rebuild_acl() + +def main(): + rebuild_acl() + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + client.on_message = on_message + client.connect(MQTT_HOST, MQTT_PORT, 60) + client.subscribe("party-lock/+/set", qos=1) + client.loop_forever() + +if __name__ == "__main__": + main()