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.
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/flowsagainstoutput/<name>.json(ignores random uuid ids)just mqtt-button [action]— E2E test: publish a button action and watch which/settopics firejust z2m-devices— paired zigbee devices + group membershipjust mqtt-pub TOPIC PAYLOAD/just mqtt-sub TOPIC/just mqtt-watch— raw MQTTjust nr-logs/just z2m-logs/just mqtt-logs/just doorbell-logs— container log tailsjust restart SERVICE/just restart-all— docker restartjust doorbell-publish "Title" "Body"— publish a test alert to ntfyjust 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.
- 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 azfield referencing the tab id. Usepolicy/flow.rego::deny_wrapped_tabto reject wrapped structure. - Every node needs
wires: []. Missing or non-arraywiresthrows TypeError inFlow.js::rewireNode.deny_missing_wiresenforces this. - MQTT brokers must have
autoConnect: true. Without it the broker never reconnects after the first user disconnects.deny_no_autoconnect. protocolVersionmust be"3","4", or"5".deny_invalid_protocol.ui-buttonmust haveicon: "". Dashboard 2.0 throws TypeError onprependIconotherwise. Generator sets this explicitly.- Dashboard widgets need a
zfield referencing their tab id, otherwise the SPA renders the group but the widgets never appear. Generator sets this. - 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.