Commit Graph

36 Commits

Author SHA1 Message Date
david 97c30acfdd deploy: device port fix, entrypoint CMD, mosquitto healthcheck 2026-07-11 04:37:06 -07:00
david ba3e172dda fix: power outage auto-recovery improvements
Mosquitto:
- Change restart: always (was unless-stopped) to survive daemon restarts
- Add healthcheck (pub/sub ping) for depends_on conditions
- All downstream services use condition: service_healthy

Party-lock:
- Add MQTT connection retry loop (was crashing on DNS race after restart)

Zigbee2mqtt:
- Add entrypoint wrapper that auto-applies BLZ bridge patch at startup
- Bridge patch no longer needs manual 'just z2m-patch-bridge' after image pulls

Mopidy:
- Add python3-gi + python3-gi-cairo via apt (fixes 'No module named gi')
- Set PYTHONPATH to expose system gi module to Python 3.12
2026-07-09 18:56:36 -07:00
david ad9a5b4e02 party-lock: use Docker SDK instead of docker kill CLI
The docker binary is not available inside the party-lock container,
so switch to the Python Docker SDK via unix socket.
2026-07-02 23:33:43 -07:00
david 67bb68b097 party-lock: fix ACL file permissions (0700, owner 1883)
Mosquitto 2.1.2 silently skips ACL file if world-readable or wrong owner.
Listener now sets proper permissions on ACL file writes.
Add CYD lock flash feedback on blocked tap.
2026-07-02 20:46:21 -07:00
david 3e06bc9e4b party-lock: global ACL block all zigbee2mqtt/+/set
Global party lock toggled via party-lock/set ON/OFF.
When locked, mosquitto ACL denies ALL zigbee2mqtt/+/set publishes.
Add lock button to CYD status bar.
2026-07-02 20:14:41 -07:00
david 36f9d0a284 party-lock: add MQTT ACL-based plug lock service
Listener watches party-lock/<plug>/set and dynamically generates
mosquitto ACL deny rules. Test against bamboo first, then dj-booth.
Add lock button to CYD dashboard.
2026-07-02 20:04:11 -07:00
david 12bfd7fb09 mopidy: use libgirepository-1.0-1 (Debian 12 slim variant package name)
python:3.12-slim-bookworm uses the older variant of libgirepository.
The package name with dashes (libgirepository-1.0-1) is what's available.
2026-07-01 08:40:15 -07:00
david 9faa5e15a1 mopidy: use libgirepository-2.0-0 (the -2.0 package variant) 2026-07-01 08:38:41 -07:00
david 9f1479edf6 mopidy: fix libgirepository package name (1.0-1 not 2.0-0)
Debian bookworm uses libgirepository1.0-1, not libgirepository-2.0-0.
The .0-0 suffix was a typo.
2026-07-01 08:36:48 -07:00
david 6ff5e91952 mopidy: add gstreamer + PyGObject apt deps
mopidy needs:
- gstreamer plugins (good/bad/ugly for audio formats)
- libgirepository + gir bindings (PyGObject) for gi module
- gstreamer-1.0 gir bindings for gst module
2026-07-01 08:35:35 -07:00
david ae088c2d9f mopidy: pin setuptools<80 (pkg_resources removed in setuptools 80+)
setuptools 80+ removed pkg_resources module. Mopidy imports pkg_resources
at startup, so we need a version that still bundles it.
2026-07-01 08:33:46 -07:00
david 0da1fbf9db mopidy: add setuptools to fix 'No module named pkg_resources'
Python 3.12 slim doesn't bundle setuptools. mopidy imports pkg_resources
from setuptools; without it, mopidy crashes on startup.
2026-07-01 08:11:03 -07:00
david e2e9492a8d librespot: add with-libmdns feature for Spotify Connect discovery
Without zeroconf backend, librespot fails with 'Credentials are required
if discovery and oauth login are disabled'. The default features include
with-libmdns but our --no-default-features build excluded it.

Add with-libmdns so the device advertises itself via mDNS and shows up
in phone Spotify apps as a Connect target.
2026-07-01 07:38:40 -07:00
david b2e44369a9 librespot: fix CMD args (--enable-audio-cache is invalid; --initial-volume)
v0.8.0 librespot uses --disable-audio-cache (audio cache is on by default).
Renamed and added --initial-volume 100 for full startup volume.
2026-07-01 07:24:50 -07:00
david 131aa9d698 librespot: remove conflicting command override (Dockerfile CMD is enough)
compose.yaml command: > syntax was being parsed as '--name' being the
executable, not an arg to librespot. Use Dockerfile CMD instead which
already includes the right args.
2026-07-01 07:23:45 -07:00
david 134fd4a0ed audio-bridge: switch /run/dbus + /run/user/0 from bind-mount to tmpfs
The host's /run/dbus/system_bus_socket is the standard dbus socket for
systemd. Bind-mounting the container's socket onto /run/dbus overwrites
the host's socket, breaking dockerd's ability to manage cgroups via
systemd (manifests as 'org.freedesktop.systemd1 was not provided by any
.service files' on subsequent docker run).

Use tmpfs instead — ephemeral container-local sockets that don't conflict
with host services.
2026-07-01 07:22:42 -07:00
david 3e730942ba audio-bridge: full pipewire-pulse.conf with context.modules
Alpine's pipewire-pulse is a minimal build that doesn't load
/usr/share/pipewire/pipewire-pulse.conf (no context.modules).
Provide a full config that includes all required modules.
2026-07-01 07:16:42 -07:00
david fff67f2baa audio-bridge: custom pipewire-pulse.conf with TCP 4713 listener
Alpine's pipewire-pulse binary is stripped (-a option removed); the
only way to configure listening address is via config file. Add
/etc/pipewire/pipewire-pulse.conf that overrides server.address to
include tcp:4713 alongside the default unix:native socket.

Server addresses now:
- unix:native (for in-container clients)
- tcp:4713 (for sibling containers connecting via PULSE_SERVER env var)
2026-07-01 07:14:20 -07:00
david 6c896f5165 audio-bridge: start pipewire-pulse separately with TCP 4713 listener
Alpine's pipewire-pulse package ships the daemon binary but doesn't
auto-load the PA bridge module on alpine. We need to start pipewire-pulse
as a separate process with -a tcp:4713 so PA clients (librespot, mopidy)
can connect via TCP.

Layout:
- pipewire: audio server (native protocol)
- pipewire-pulse: PA-compat bridge, listens on tcp:4713 + unix:/tmp/pulse-socket
- wireplumber: session manager (loads BT modules, hot-plug handling)
2026-07-01 07:12:02 -07:00
david 6b55a16581 audio-bridge: remove --print-pid from dbus session (broken in containers)
dbus-daemon --print-pid=N opens fd N for writing; under tini this fails
with 'Writing to pipe: Invalid argument'. Just use --fork for daemonization.
2026-07-01 07:09:54 -07:00
david c2f14b60aa audio-bridge: fix bluetoothd path + add session dbus + cleanup stale pid
- bluetoothd at /usr/lib/bluetooth/bluetoothd (not in PATH on alpine)
- Add session dbus via dbus-daemon --session (pipewire needs it)
- Clean up stale dbus.pid files on startup (host volume persists)
2026-07-01 07:06:25 -07:00
david 32b9189f2f librespot: add native-tls feature (required by librespot-oauth)
oauth2 crate gates TLS backends behind features; without native-tls,
librespot-oauth fails to compile with compile_error!. The default
features include native-tls, but we use --no-default-features for
minimal build.
2026-07-01 05:52:07 -07:00
david e5b52fa56f librespot: switch from cross-compile to native ARM64 build
Cross-compile on x86_64 with qemu emulation takes >30min and hits
60-90min ceiling due to slow crate-by-crate aarch64 build under qemu.
Native ARM64 build on RPi 3B+ with --jobs 2 (1GB RAM) is much faster.

Rust 1.85.1 installed via rustup on RPi (precompiled aarch64 toolchain).
2026-07-01 05:00:22 -07:00
david 573821426c audio: add PipeWire+BT audio-bridge, mopidy (Qobuz+local), librespot (Spotify Connect)
- audio-bridge: alpine + pipewire + pipewire-pulse + wireplumber + bluez + dbus + tini
  Routes audio from librespot/mopidy/phone-BT to USB DAC (Q5K) via ALSA.
  Privileged + host network for bluetoothd HCI access.
- mopidy: built from python:3.12-slim-bookworm + pip (no ghcr.io dep).
  Qobuz (auth from developer.qobuz.com) + local files. PA bridge to audio-bridge.
- librespot: built from rust:1.85.1 on RPi (cross-compile too slow under qemu).
  Spotify Connect target named 'Klubhaus'. PA bridge to audio-bridge.

Both mopidy and librespot build locally to avoid ghcr.io auth/rate-limit issues.
PEQ stays on Q5K side (Qudelix app, 20 bands) for source-independent EQ.
2026-07-01 04:49:15 -07:00
david 09a4328960 doorbell-listener: poll every 5s (was 30s) for tighter lag
ntfy /json?poll=1 is regular polling, not long-poll. Worst-case
lag = 2 × POLL_INTERVAL × N_topics = 120s at 30s. Reducing to 5s
caps lag at ~20s with 2 topics. For sub-second latency, switch to
ntfy SSE (/<topic>/sse) - tracked separately.
2026-07-01 02:46:40 -07:00
david 7ed170acd4 doorbell-listener: 1s flash interval (was 2s), 14 toggles total
Each blink is now 1s (was 2s); 14 toggles over 13s gives sharper
'rapid fire' cadence. Even count keeps the plug ending at its
starting state.
2026-07-01 02:45:23 -07:00
david 66bc573dfd nodered: inline-write start.sh to avoid COPY+chmod permission issue
The NR base image's /data has restrictive ACLs that block chmod on
COPY'd files even as root. Writing start.sh inline with printf + chmod
sidesteps this.
2026-07-01 02:13:23 -07:00
david 1ac9ef948b nodered: bake Dashboard 2.0 into Dockerfile for idempotent install
NR's automatic package.json install at container start was slow and
required internet. Move npm install to image build time, preserve the
installed node_modules via an anonymous volume so the bind mount of
./nodered:/data doesn't clobber them.
2026-07-01 01:50:39 -07:00
david 3f0c85c41b zigbee2mqtt: add per-device pairing notes (3RSB22BZ button modes)
Document the ThirdReality Smart Button (3RSB22BZ) pairing mode quirk
that bit us today: the button has Standard ZigBee (blue only) and
Echo ZigBee (blue + red) modes, and only the former works with
zigbee2mqtt. Switching requires 5 quick presses of the back reset
button (firmware >= 1.00.22), NOT holding the front button.

Also document the Smart Plug Zigbee PROD pairing procedure and the
BLE->ZigBee mode switch.
2026-07-01 01:35:04 -07:00
david c2de5ba85b zigbee2mqtt: patch Bridge TypeError on BLZ coordinator
The Pine64 BLZ fork's zigbee2mqtt image crashes on start because
Bridge.start() calls firstCoordinatorEndpoint().deviceIeeeAddress
before the onMQTTMessage listener is registered. The BLZ coordinator
does not register its endpoint (the coordinator is the dongle itself,
no ZDO loopback), so firstCoordinatorEndpoint() returns undefined.

The crash happens in publishInfo() BEFORE onMQTTMessage is wired up,
which means every bridge MQTT request (permit_join, device rename,
group ops, etc.) silently no-ops — even though the message is
received.

Add infra/zigbee2mqtt/scripts/blz-bridge-patch.js that applies two
surgical fixes (idempotent):

  1. Optional chain on the .deviceIeeeAddress access so the missing
     endpoint doesn't throw.
  2. Move onMQTTMessage registration BEFORE publishInfo/devices/
     groups/definitions, and wrap each publish in try/catch so
     Bridge.start() completes even if a single publish fails.

Add 'just z2m-patch-bridge' to apply the patch on the live container
after fresh image pulls.
2026-07-01 01:22:56 -07:00
david 758edc0a9a infra: tune flash params + persist zigbee network key for paired devices
- FLASH_INTERVAL_SECONDS 3 -> 2 (user preference)
- Add network_key / pan_id / ext_pan_id to zigbee2mqtt configuration
  so paired devices can rejoin after a config wipe. Network key
  preserved from previous working state on RPi.
- Doorbell listener flash logic with even toggle count so plug ends
  at starting state
2026-07-01 01:11:41 -07:00
david 1ef49652b0 doorbell-listener: flash plug on alert (configurable interval/duration)
On a fresh (< MAX_AGE_SECONDS) ntfy message, spawn a thread that
publishes N TOGGLEs spaced FLASH_INTERVAL_SECONDS apart so total
runtime is FLASH_DURATION_SECONDS. Defaults: 5 toggles @ 3s = 15s.

Env vars (all in compose.yaml):
  FLASH_INTERVAL_SECONDS  default 3
  FLASH_DURATION_SECONDS  default 15
  MAX_AGE_SECONDS         default 180 (replay protection)

Each alert gets its own thread; concurrent alerts spawn concurrent
flashes. z2m just flips state per toggle, so the lamp alternates
regardless of overlap.
2026-07-01 00:41:48 -07:00
david c9ea2b9393 doorbell-listener: add PEP 723 inline deps for uv auto-resolve
uv run without a project file (pyproject.toml/requirements.txt) won't
auto-resolve from script imports — needs inline metadata per PEP 723.

Fix: add # /// script block declaring requests + paho-mqtt>=2.0.
2026-07-01 00:35:35 -07:00
david 90927ccc0e Add doorbell-listener: ntfy → zigbee2mqtt Sideboard Lamp toggle
Polls ntfy.sh for ALERT_klubhaus_topic(_test) and publishes
{state: TOGGLE} to zigbee2mqtt/Sideboard Lamp/set on each new
message event. Dual-topic subscription so switching from test to
production requires no code change; sender still uses test topic.

- infra/doorbell-listener/: uv-based Docker service (arm64)
- compose: depends_on mosquitto, persists last_id per topic
- justfile: doorbell-publish, doorbell-logs, doorbell-rebuild,
  restart-all now includes doorbell-listener
2026-07-01 00:30:08 -07:00
david 60ac42e711 Split button: single→3 plugs, hold-release→DJ Booth only
zigbee-monitor.yaml:
- Single click (action=single) toggles Squiggle, Sideboard Lamp,
  Bamboo Lights
- Press-hold-release (action=release after hold) toggles DJ Booth
  only
- Two filter functions: "Single click" and "Hold release", button-in
  fan-out wires to both
- DJ Booth added to zigbee-state-in and ui-text widgets for visibility
- "plugs:" list replaced with "single_plugs:" and "hold_plugs:" for
  declarative clarity (generator doesn't use them, but documents intent)

infra/zigbee2mqtt/configuration.yaml:
- Removed DJ Booth from "all_plugs" group (now Squiggle, Sideboard
  Lamp, Bamboo Lights only)
- Pushed same change to live /root/zigbee2mqtt/configuration.yaml and
  restarted zigbee2mqtt container

Diagnostics in justfile + scripts/:

justfile:
- Constants: RPI_HOST, MQTT_HOST, MQTT_PORT, RPI_DIR, NR_PORT
- rpi, mqtt-pub, mqtt-sub, mqtt-watch, mqtt-button
- nr-show, nr-diff, nr-logs, nr-editor, nr-dashboard
- z2m-devices, z2m-logs, z2m-state
- mqtt-logs, restart, restart-all

scripts/mqtt.py:
- MQTT publish/subscribe/watch helpers using paho-mqtt v2.x
- Run via uv run --with paho-mqtt (no global install needed)

scripts/diag.py:
- nr-show: live flow summary with mqtt-out topic list
- nr-diff: structural diff deployed vs repo, normalizes random uuids
- z2m-devices: paired devices + group membership
- mqtt-button: publish action and watch /set topics (E2E test)

Verified end-to-end via `just mqtt-button`:
  single  → 3 messages on .../set (Squiggle, Sideboard Lamp, Bamboo Lights)
  release → 1 message on .../set (DJ Booth)
2026-06-30 23:28:13 -07:00
david de260eec62 Add infra/ — full stack config as deployed on RPi 3B+
Captures everything needed to rebuild the stack from scratch:

infra/compose.yaml
- mosquitto (eclipse-mosquitto:latest)
- zigbee2mqtt (ghcr.io/pine64/zigbee2mqtt:latest-dev, BLZ fork)
- nodered (nodered/node-red:latest)

infra/mosquitto/config/mosquitto.conf
- anonymous listener on 1883, file logging, persistence on

infra/zigbee2mqtt/
- configuration.yaml: 5 paired devices with friendly names, 1 group
- coordinator_backup.json: zigbee network key + pan id (recovery)
- state.json: per-device persisted state (ON/OFF, linkquality)

infra/nodered/
- settings.js: nodered defaults (flowFile: flows.json, no auth)
- start.sh: patches Dashboard 2.0 SPA with <base href> at startup

Also:
- Updated z2m groups section to rename Laser & Fog -> DJ Booth
  (matches the friendly_name already in devices section)
- Pushed the same change to live /root/zigbee2mqtt/configuration.yaml
  and restarted the zigbee2mqtt container
- .gitignore: output/ (regenerated on every build, churns uuid ids)
- README.md: link to infra/README.md for rebuild procedure
2026-06-30 23:19:37 -07:00