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

79 lines
4.3 KiB
Markdown

# 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
```bash
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.