Commit Graph

17 Commits

Author SHA1 Message Date
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