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.
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.