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
Closes#1.
Generator (scripts/flow2json.py):
- Emit all nodes at top level with z field (no wrapped tabs)
- Set icon: "" on ui-button to avoid Dashboard 2.0 prependIcon TypeError
- Set z field on ui-* widgets so they render in Dashboard 2.0 SPA
- Use ui-text label from spec, not name
Policy (policy/flow.rego) — 9 new deny rules:
- deny_wrapped_tab: tab with internal nodes[] (nodered 5.0 bug)
- deny_missing_wires: missing or non-array wires (Flow.js rewireNode crash)
- deny_no_autoconnect: mqtt-broker must reconnect
- deny_invalid_protocol: protocolVersion must be 3/4/5
- deny_unused_broker: broker declared but never referenced
- Plus deny_broker_port / deny_group_ref / deny_page_ref / deny_wire_ref refinements
Flow (zigbee-monitor.*):
- 22-node flat flow: button-in → btn-filter → 4 parallel plug-outs
- 4 ui-text widgets + ui-button for permit-join (60s)
- Renamed plug: "Laser & Fog" → "DJ Booth" (matches z2m friendly_name)
Repo hygiene:
- justfile: fix host IP 192.168.9.147 → 192.168.81.147, /ui → /dashboard
- README.md: document 8 generation invariants and rule table
Verified end-to-end: publishing {"action":"single"} on
zigbee2mqtt/Door Button triggers {"state":"TOGGLE"} on all four
.../set topics including the renamed DJ Booth.
462/462 conftest tests pass.
Button (Door Button) now toggles Squiggle, Sideboard Lamp, and Bamboo Lights
via three parallel mqtt-out publishes.
Key fix: avoid subscribing to a wildcard '#' topic alongside a specific topic
in the same Node-RED instance. The mqtt-broker re-adds its handler as an
EventEmitter listener on each subscribe call (file:
/usr/src/node-red/node_modules/@node-red/nodes/core/network/10-mqtt.js
_clientOn('message', subscription.handler) is unconditional), so after
each deploy/restart the same message arrives N times to internal listeners.
Each mqtt-in node now uses a unique topic (zigbee2mqtt/<device>).
Removed zigbee-in # wildcard and plug-state ui-text widgets (state now
displayed via per-plug ui-text widgets).
Added function node support to flow2json.py for msg filtering logic.
Added ui-text format/width/height defaults support.
Added dashboard 2.0 config node defaults matching reference flow:
ui-base, ui-theme, ui-page, ui-group with full property set.