Files
nr-flow-validator/README.md
T
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

4.3 KiB

nr-flow-validator

Declarative Node-RED flows with conftest/Rego validation. Generates flows.json from a YAML spec, validates against a Rego policy pack, deploys via the Node-RED HTTP API.

Layout

infra/                           # host stack config (compose, z2m, mosquitto, nodered settings)
flows/<name>.yaml                # declarative spec
scripts/flow2json.py             # YAML → JSON generator
policy/flow.rego                 # Rego policy pack (conftest)
output/<name>.json               # generated flows.json

Quick start

just new my-flow           # copy template
$EDITOR flows/my-flow.yaml
just validate              # generate + validate
just deploy                # push to Node-RED at 192.168.81.147:1880

See infra/README.md for the full Docker stack and rebuild procedure.

Diagnostics

All diagnostic commands live in the justfile. Run just --list to see them.

  • just nr-show — live flow summary (node count, functions, button wiring, mqtt-out topics)
  • just nr-diff — diff deployed /flows against output/<name>.json (ignores random uuid ids)
  • just mqtt-button [action] — E2E test: publish a button action and watch which /set topics fire
  • just z2m-devices — paired zigbee devices + group membership
  • just mqtt-pub TOPIC PAYLOAD / just mqtt-sub TOPIC / just mqtt-watch — raw MQTT
  • just nr-logs / just z2m-logs / just mqtt-logs — container log tails
  • just restart SERVICE / just restart-all — docker restart
  • just rpi CMD — run any command on the RPi over SSH

scripts/mqtt.py and scripts/diag.py are the underlying helpers. Override the target host with just RPI_HOST=user@host mqtt-button single.

Generation invariants

These are enforced by policy/flow.rego and burned in from real bugs found in deployment. The generator follows them by construction; the policy is the second line of defense.

  1. Flat structure only. Node-RED 5.0 silently breaks MQTT broker connection when a tab has an internal nodes: [...] array. Always emit all nodes at top level with a z field referencing the tab id. Use policy/flow.rego::deny_wrapped_tab to reject wrapped structure.
  2. Every node needs wires: []. Missing or non-array wires throws TypeError in Flow.js::rewireNode. deny_missing_wires enforces this.
  3. MQTT brokers must have autoConnect: true. Without it the broker never reconnects after the first user disconnects. deny_no_autoconnect.
  4. protocolVersion must be "3", "4", or "5". deny_invalid_protocol.
  5. ui-button must have icon: "". Dashboard 2.0 throws TypeError on prependIcon otherwise. Generator sets this explicitly.
  6. Dashboard widgets need a z field referencing their tab id, otherwise the SPA renders the group but the widgets never appear. Generator sets this.
  7. Wire references must resolve to known node ids. deny_wire_ref.

Validation rules summary

Rule Catches
deny_missing_type node without type
deny_missing_field required field missing for type
deny_broker_ref mqtt in/out references unknown broker
deny_group_ref ui-* references unknown group
deny_page_ref ui-group references unknown page
deny_wire_ref wire targets unknown node
deny_missing_flow wire references a node not in z of a tab
deny_isolated_node node with no wires in/out (config nodes exempted)
deny_broker_port mqtt-broker port outside 1-65535
deny_wrapped_tab tab with internal nodes array (nodered 5.0 bug)
deny_missing_wires node without wires array
deny_no_autoconnect mqtt-broker without autoConnect: true
deny_invalid_protocol mqtt-broker protocolVersion not in 3/4/5
deny_unused_broker broker defined but never referenced

Deployment target

Node-RED on RPi 3B+ DietPi at 192.168.81.147:1880, persisted via Docker bind mount to /root/nodered. The deploy target's flows.json is the source of truth in production; this generator is for declarative editing.

Dashboard 2.0 SPA quirk

Dashboard 2.0's dist/index.html ships with relative asset paths that break at /dashboard/home. /data/start.sh on the host patches in <base href="/dashboard/"> on container start. Without that patch the SPA loads but assets 404 and nothing renders.