Flat-structure generator + Rego guards, rename Laser & Fog to DJ Booth
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.
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -9,6 +9,7 @@ plugs:
|
|||||||
- Squiggle
|
- Squiggle
|
||||||
- Sideboard Lamp
|
- Sideboard Lamp
|
||||||
- Bamboo Lights
|
- Bamboo Lights
|
||||||
|
- DJ Booth
|
||||||
|
|
||||||
dashboards:
|
dashboards:
|
||||||
- base: Dashboard
|
- base: Dashboard
|
||||||
@@ -22,7 +23,7 @@ dashboards:
|
|||||||
|
|
||||||
flows:
|
flows:
|
||||||
- tab: Zigbee Monitor
|
- tab: Zigbee Monitor
|
||||||
description: Monitor and control zigbee2mqtt coordinator
|
description: Single button toggles all plugs
|
||||||
nodes:
|
nodes:
|
||||||
- id: zigbee-state-squiggle
|
- id: zigbee-state-squiggle
|
||||||
type: mqtt in
|
type: mqtt in
|
||||||
@@ -42,6 +43,12 @@ flows:
|
|||||||
topic: zigbee2mqtt/Bamboo Lights
|
topic: zigbee2mqtt/Bamboo Lights
|
||||||
qos: 0
|
qos: 0
|
||||||
|
|
||||||
|
- id: zigbee-state-laser
|
||||||
|
type: mqtt in
|
||||||
|
broker: mosquitto
|
||||||
|
topic: zigbee2mqtt/DJ Booth
|
||||||
|
qos: 0
|
||||||
|
|
||||||
- id: button-in
|
- id: button-in
|
||||||
type: mqtt in
|
type: mqtt in
|
||||||
broker: mosquitto
|
broker: mosquitto
|
||||||
@@ -50,7 +57,7 @@ flows:
|
|||||||
|
|
||||||
- id: btn-filter
|
- id: btn-filter
|
||||||
type: function
|
type: function
|
||||||
name: Single press only
|
name: Single press
|
||||||
code: |
|
code: |
|
||||||
try {
|
try {
|
||||||
const p = JSON.parse(msg.payload);
|
const p = JSON.parse(msg.payload);
|
||||||
@@ -76,23 +83,38 @@ flows:
|
|||||||
broker: mosquitto
|
broker: mosquitto
|
||||||
topic: zigbee2mqtt/Bamboo Lights/set
|
topic: zigbee2mqtt/Bamboo Lights/set
|
||||||
|
|
||||||
|
- id: plug-out-laser
|
||||||
|
type: mqtt out
|
||||||
|
broker: mosquitto
|
||||||
|
topic: zigbee2mqtt/DJ Booth/set
|
||||||
|
|
||||||
- id: text-squiggle
|
- id: text-squiggle
|
||||||
type: ui-text
|
type: ui-text
|
||||||
group: Plugs
|
group: Plugs
|
||||||
label: Squiggle
|
label: Squiggle
|
||||||
format: "{{msg.payload.state}}"
|
format: "{{msg.payload.state}}"
|
||||||
|
order: 1
|
||||||
|
|
||||||
- id: text-sideboard
|
- id: text-sideboard
|
||||||
type: ui-text
|
type: ui-text
|
||||||
group: Plugs
|
group: Plugs
|
||||||
label: Sideboard Lamp
|
label: Sideboard Lamp
|
||||||
format: "{{msg.payload.state}}"
|
format: "{{msg.payload.state}}"
|
||||||
|
order: 2
|
||||||
|
|
||||||
- id: text-bamboo
|
- id: text-bamboo
|
||||||
type: ui-text
|
type: ui-text
|
||||||
group: Plugs
|
group: Plugs
|
||||||
label: Bamboo Lights
|
label: Bamboo Lights
|
||||||
format: "{{msg.payload.state}}"
|
format: "{{msg.payload.state}}"
|
||||||
|
order: 3
|
||||||
|
|
||||||
|
- id: text-laser
|
||||||
|
type: ui-text
|
||||||
|
group: Plugs
|
||||||
|
label: DJ Booth
|
||||||
|
format: "{{msg.payload.state}}"
|
||||||
|
order: 4
|
||||||
|
|
||||||
- id: permit-btn
|
- id: permit-btn
|
||||||
type: ui-button
|
type: ui-button
|
||||||
@@ -102,6 +124,7 @@ flows:
|
|||||||
payload:
|
payload:
|
||||||
value: true
|
value: true
|
||||||
time: 60
|
time: 60
|
||||||
|
order: 5
|
||||||
|
|
||||||
- id: mqtt-out
|
- id: mqtt-out
|
||||||
type: mqtt out
|
type: mqtt out
|
||||||
@@ -111,6 +134,7 @@ flows:
|
|||||||
zigbee-state-squiggle: [text-squiggle]
|
zigbee-state-squiggle: [text-squiggle]
|
||||||
zigbee-state-sideboard: [text-sideboard]
|
zigbee-state-sideboard: [text-sideboard]
|
||||||
zigbee-state-bamboo: [text-bamboo]
|
zigbee-state-bamboo: [text-bamboo]
|
||||||
|
zigbee-state-laser: [text-laser]
|
||||||
button-in: [btn-filter]
|
button-in: [btn-filter]
|
||||||
btn-filter: [plug-out-squiggle, plug-out-sideboard, plug-out-bamboo]
|
btn-filter: [plug-out-squiggle, plug-out-sideboard, plug-out-bamboo, plug-out-laser]
|
||||||
permit-btn: [mqtt-out]
|
permit-btn: [mqtt-out]
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ validate-all:
|
|||||||
# ---- Deployment ----
|
# ---- Deployment ----
|
||||||
|
|
||||||
# Deploy flow to Node-RED instance
|
# Deploy flow to Node-RED instance
|
||||||
deploy FLOW="zigbee-monitor" HOST="http://192.168.9.147:1880": generate validate
|
deploy FLOW="zigbee-monitor" HOST="http://192.168.81.147:1880": generate validate
|
||||||
@echo "=== Deploying {{FLOW}} to {{HOST}} ==="
|
@echo "=== Deploying {{FLOW}} to {{HOST}} ==="
|
||||||
curl -s -o /dev/null -w "HTTP %{http_code}" \
|
curl -s -o /dev/null -w "HTTP %{http_code}" \
|
||||||
-X POST {{HOST}}/flows \
|
-X POST {{HOST}}/flows \
|
||||||
@@ -47,7 +47,7 @@ deploy FLOW="zigbee-monitor" HOST="http://192.168.9.147:1880": generate validate
|
|||||||
@echo ""
|
@echo ""
|
||||||
|
|
||||||
# Quick deploy (skip validation)
|
# Quick deploy (skip validation)
|
||||||
deploy-quick FLOW="zigbee-monitor" HOST="http://192.168.9.147:1880": generate
|
deploy-quick FLOW="zigbee-monitor" HOST="http://192.168.81.147:1880": generate
|
||||||
@echo "=== Quick deploy {{FLOW}} to {{HOST}} ==="
|
@echo "=== Quick deploy {{FLOW}} to {{HOST}} ==="
|
||||||
curl -s -o /dev/null -w "HTTP %{http_code}" \
|
curl -s -o /dev/null -w "HTTP %{http_code}" \
|
||||||
-X POST {{HOST}}/flows \
|
-X POST {{HOST}}/flows \
|
||||||
@@ -59,12 +59,12 @@ deploy-quick FLOW="zigbee-monitor" HOST="http://192.168.9.147:1880": generate
|
|||||||
# ---- Viewing ----
|
# ---- Viewing ----
|
||||||
|
|
||||||
# Fetch and display current flows from Node-RED
|
# Fetch and display current flows from Node-RED
|
||||||
pull HOST="http://192.168.9.147:1880":
|
pull HOST="http://192.168.81.147:1880":
|
||||||
curl -s {{HOST}}/flows | python3 -m json.tool
|
curl -s {{HOST}}/flows | python3 -m json.tool
|
||||||
|
|
||||||
# Open dashboard in browser
|
# Open dashboard in browser
|
||||||
dashboard:
|
dashboard:
|
||||||
xdg-open http://192.168.9.147:1880/ui 2>/dev/null || open http://192.168.9.147:1880/ui 2>/dev/null || echo "Open: http://192.168.9.147:1880/ui"
|
xdg-open http://192.168.81.147:1880/dashboard 2>/dev/null || open http://192.168.81.147:1880/dashboard 2>/dev/null || echo "Open: http://192.168.81.147:1880/dashboard"
|
||||||
|
|
||||||
# ---- Development ----
|
# ---- Development ----
|
||||||
|
|
||||||
|
|||||||
+114
-56
@@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "br9dfef2b2",
|
"id": "braff51339",
|
||||||
"type": "mqtt-broker",
|
"type": "mqtt-broker",
|
||||||
"name": "mosquitto",
|
"name": "mosquitto",
|
||||||
"broker": "mosquitto",
|
"broker": "mosquitto",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"willPayload": ""
|
"willPayload": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "uibba9906e7",
|
"id": "uibab06e3f4",
|
||||||
"type": "ui-base",
|
"type": "ui-base",
|
||||||
"name": "Dashboard",
|
"name": "Dashboard",
|
||||||
"path": "/dashboard",
|
"path": "/dashboard",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"titleBarStyle": "default"
|
"titleBarStyle": "default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "uitcdb20b5e",
|
"id": "uit4a70230a",
|
||||||
"type": "ui-theme",
|
"type": "ui-theme",
|
||||||
"name": "Default Theme",
|
"name": "Default Theme",
|
||||||
"colors": {
|
"colors": {
|
||||||
@@ -54,13 +54,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "uipb062fd21",
|
"id": "uip2851213f",
|
||||||
"type": "ui-page",
|
"type": "ui-page",
|
||||||
"name": "Home",
|
"name": "Home",
|
||||||
"ui": "uibba9906e7",
|
"ui": "uibab06e3f4",
|
||||||
"icon": "home",
|
"icon": "home",
|
||||||
"path": "/home",
|
"path": "/home",
|
||||||
"theme": "uitcdb20b5e",
|
"theme": "uit4a70230a",
|
||||||
"layout": "flex",
|
"layout": "flex",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"className": "",
|
"className": "",
|
||||||
@@ -68,10 +68,10 @@
|
|||||||
"disabled": false
|
"disabled": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "uig1ba3cd58",
|
"id": "uig93b85217",
|
||||||
"type": "ui-group",
|
"type": "ui-group",
|
||||||
"name": "Plugs",
|
"name": "Plugs",
|
||||||
"page": "uipb062fd21",
|
"page": "uip2851213f",
|
||||||
"width": "6",
|
"width": "6",
|
||||||
"height": "1",
|
"height": "1",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
@@ -80,9 +80,16 @@
|
|||||||
"visible": true,
|
"visible": true,
|
||||||
"disabled": false
|
"disabled": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "fd235da06",
|
||||||
|
"type": "tab",
|
||||||
|
"label": "Zigbee Monitor",
|
||||||
|
"disabled": false,
|
||||||
|
"info": "Single button toggles all plugs",
|
||||||
|
"env": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "zigbee-state-squiggle",
|
"id": "zigbee-state-squiggle",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "mqtt in",
|
"type": "mqtt in",
|
||||||
"wires": [
|
"wires": [
|
||||||
[
|
[
|
||||||
@@ -92,14 +99,14 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"x": 170,
|
"x": 170,
|
||||||
"y": 80,
|
"y": 80,
|
||||||
"broker": "br9dfef2b2",
|
"broker": "braff51339",
|
||||||
"topic": "zigbee2mqtt/Squiggle",
|
"topic": "zigbee2mqtt/Squiggle",
|
||||||
"qos": "0",
|
"qos": "0",
|
||||||
"datatype": "auto"
|
"datatype": "auto",
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "zigbee-state-sideboard",
|
"id": "zigbee-state-sideboard",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "mqtt in",
|
"type": "mqtt in",
|
||||||
"wires": [
|
"wires": [
|
||||||
[
|
[
|
||||||
@@ -109,14 +116,14 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"x": 170,
|
"x": 170,
|
||||||
"y": 80,
|
"y": 80,
|
||||||
"broker": "br9dfef2b2",
|
"broker": "braff51339",
|
||||||
"topic": "zigbee2mqtt/Sideboard Lamp",
|
"topic": "zigbee2mqtt/Sideboard Lamp",
|
||||||
"qos": "0",
|
"qos": "0",
|
||||||
"datatype": "auto"
|
"datatype": "auto",
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "zigbee-state-bamboo",
|
"id": "zigbee-state-bamboo",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "mqtt in",
|
"type": "mqtt in",
|
||||||
"wires": [
|
"wires": [
|
||||||
[
|
[
|
||||||
@@ -126,14 +133,31 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"x": 170,
|
"x": 170,
|
||||||
"y": 80,
|
"y": 80,
|
||||||
"broker": "br9dfef2b2",
|
"broker": "braff51339",
|
||||||
"topic": "zigbee2mqtt/Bamboo Lights",
|
"topic": "zigbee2mqtt/Bamboo Lights",
|
||||||
"qos": "0",
|
"qos": "0",
|
||||||
"datatype": "auto"
|
"datatype": "auto",
|
||||||
|
"z": "fd235da06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zigbee-state-laser",
|
||||||
|
"type": "mqtt in",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"text-laser"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "",
|
||||||
|
"x": 170,
|
||||||
|
"y": 220,
|
||||||
|
"broker": "braff51339",
|
||||||
|
"topic": "zigbee2mqtt/DJ Booth",
|
||||||
|
"qos": "0",
|
||||||
|
"datatype": "auto",
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "button-in",
|
"id": "button-in",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "mqtt in",
|
"type": "mqtt in",
|
||||||
"wires": [
|
"wires": [
|
||||||
[
|
[
|
||||||
@@ -143,23 +167,24 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"x": 170,
|
"x": 170,
|
||||||
"y": 220,
|
"y": 220,
|
||||||
"broker": "br9dfef2b2",
|
"broker": "braff51339",
|
||||||
"topic": "zigbee2mqtt/Door Button",
|
"topic": "zigbee2mqtt/Door Button",
|
||||||
"qos": "0",
|
"qos": "0",
|
||||||
"datatype": "auto"
|
"datatype": "auto",
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "btn-filter",
|
"id": "btn-filter",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"wires": [
|
"wires": [
|
||||||
[
|
[
|
||||||
"plug-out-squiggle",
|
"plug-out-squiggle",
|
||||||
"plug-out-sideboard",
|
"plug-out-sideboard",
|
||||||
"plug-out-bamboo"
|
"plug-out-bamboo",
|
||||||
|
"plug-out-laser"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"name": "Single press only",
|
"name": "Single press",
|
||||||
"x": 450,
|
"x": 450,
|
||||||
"y": 220,
|
"y": 220,
|
||||||
"func": "try {\n const p = JSON.parse(msg.payload);\n if (p.action === 'single') {\n msg.payload = { state: 'TOGGLE' };\n return msg;\n }\n} catch (e) {}\nreturn null;\n",
|
"func": "try {\n const p = JSON.parse(msg.payload);\n if (p.action === 'single') {\n msg.payload = { state: 'TOGGLE' };\n return msg;\n }\n} catch (e) {}\nreturn null;\n",
|
||||||
@@ -167,101 +192,131 @@
|
|||||||
"noerr": 0,
|
"noerr": 0,
|
||||||
"initialize": "",
|
"initialize": "",
|
||||||
"finalize": "",
|
"finalize": "",
|
||||||
"libs": []
|
"libs": [],
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "plug-out-squiggle",
|
"id": "plug-out-squiggle",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "mqtt out",
|
"type": "mqtt out",
|
||||||
"wires": [],
|
"wires": [],
|
||||||
"name": "",
|
"name": "",
|
||||||
"x": 770,
|
"x": 770,
|
||||||
"y": 220,
|
"y": 360,
|
||||||
"broker": "br9dfef2b2",
|
"broker": "braff51339",
|
||||||
"topic": "zigbee2mqtt/Squiggle/set",
|
"topic": "zigbee2mqtt/Squiggle/set",
|
||||||
"qos": "2",
|
"qos": "2",
|
||||||
"retain": false
|
"retain": false,
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "plug-out-sideboard",
|
"id": "plug-out-sideboard",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "mqtt out",
|
"type": "mqtt out",
|
||||||
"wires": [],
|
"wires": [],
|
||||||
"name": "",
|
"name": "",
|
||||||
"x": 770,
|
"x": 770,
|
||||||
"y": 360,
|
"y": 360,
|
||||||
"broker": "br9dfef2b2",
|
"broker": "braff51339",
|
||||||
"topic": "zigbee2mqtt/Sideboard Lamp/set",
|
"topic": "zigbee2mqtt/Sideboard Lamp/set",
|
||||||
"qos": "2",
|
"qos": "2",
|
||||||
"retain": false
|
"retain": false,
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "plug-out-bamboo",
|
"id": "plug-out-bamboo",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "mqtt out",
|
"type": "mqtt out",
|
||||||
"wires": [],
|
"wires": [],
|
||||||
"name": "",
|
"name": "",
|
||||||
"x": 770,
|
"x": 770,
|
||||||
"y": 360,
|
"y": 360,
|
||||||
"broker": "br9dfef2b2",
|
"broker": "braff51339",
|
||||||
"topic": "zigbee2mqtt/Bamboo Lights/set",
|
"topic": "zigbee2mqtt/Bamboo Lights/set",
|
||||||
"qos": "2",
|
"qos": "2",
|
||||||
"retain": false
|
"retain": false,
|
||||||
|
"z": "fd235da06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plug-out-laser",
|
||||||
|
"type": "mqtt out",
|
||||||
|
"wires": [],
|
||||||
|
"name": "",
|
||||||
|
"x": 770,
|
||||||
|
"y": 500,
|
||||||
|
"broker": "braff51339",
|
||||||
|
"topic": "zigbee2mqtt/DJ Booth/set",
|
||||||
|
"qos": "2",
|
||||||
|
"retain": false,
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "text-squiggle",
|
"id": "text-squiggle",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "ui-text",
|
"type": "ui-text",
|
||||||
"wires": [],
|
"wires": [],
|
||||||
"name": "Squiggle",
|
"name": "Squiggle",
|
||||||
"x": 770,
|
"x": 770,
|
||||||
"y": 360,
|
"y": 500,
|
||||||
"group": "uig1ba3cd58",
|
"group": "uig93b85217",
|
||||||
"width": "6",
|
"width": "6",
|
||||||
"height": "2",
|
"height": "2",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"label": "Squiggle",
|
"label": "Squiggle",
|
||||||
"format": "{{msg.payload.state}}",
|
"format": "{{msg.payload.state}}",
|
||||||
"layout": "row-spread",
|
"layout": "row-spread",
|
||||||
"className": ""
|
"className": "",
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "text-sideboard",
|
"id": "text-sideboard",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "ui-text",
|
"type": "ui-text",
|
||||||
"wires": [],
|
"wires": [],
|
||||||
"name": "Sideboard Lamp",
|
"name": "Sideboard Lamp",
|
||||||
"x": 770,
|
"x": 770,
|
||||||
"y": 500,
|
"y": 500,
|
||||||
"group": "uig1ba3cd58",
|
"group": "uig93b85217",
|
||||||
"width": "6",
|
"width": "6",
|
||||||
"height": "2",
|
"height": "2",
|
||||||
"order": 1,
|
"order": 2,
|
||||||
"label": "Sideboard Lamp",
|
"label": "Sideboard Lamp",
|
||||||
"format": "{{msg.payload.state}}",
|
"format": "{{msg.payload.state}}",
|
||||||
"layout": "row-spread",
|
"layout": "row-spread",
|
||||||
"className": ""
|
"className": "",
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "text-bamboo",
|
"id": "text-bamboo",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "ui-text",
|
"type": "ui-text",
|
||||||
"wires": [],
|
"wires": [],
|
||||||
"name": "Bamboo Lights",
|
"name": "Bamboo Lights",
|
||||||
"x": 770,
|
"x": 770,
|
||||||
"y": 500,
|
"y": 640,
|
||||||
"group": "uig1ba3cd58",
|
"group": "uig93b85217",
|
||||||
"width": "6",
|
"width": "6",
|
||||||
"height": "2",
|
"height": "2",
|
||||||
"order": 1,
|
"order": 3,
|
||||||
"label": "Bamboo Lights",
|
"label": "Bamboo Lights",
|
||||||
"format": "{{msg.payload.state}}",
|
"format": "{{msg.payload.state}}",
|
||||||
"layout": "row-spread",
|
"layout": "row-spread",
|
||||||
"className": ""
|
"className": "",
|
||||||
|
"z": "fd235da06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-laser",
|
||||||
|
"type": "ui-text",
|
||||||
|
"wires": [],
|
||||||
|
"name": "DJ Booth",
|
||||||
|
"x": 770,
|
||||||
|
"y": 640,
|
||||||
|
"group": "uig93b85217",
|
||||||
|
"width": "6",
|
||||||
|
"height": "2",
|
||||||
|
"order": 4,
|
||||||
|
"label": "DJ Booth",
|
||||||
|
"format": "{{msg.payload.state}}",
|
||||||
|
"layout": "row-spread",
|
||||||
|
"className": "",
|
||||||
|
"z": "fd235da06"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "permit-btn",
|
"id": "permit-btn",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "ui-button",
|
"type": "ui-button",
|
||||||
"wires": [
|
"wires": [
|
||||||
[
|
[
|
||||||
@@ -270,27 +325,30 @@
|
|||||||
],
|
],
|
||||||
"name": "Permit Join (60s)",
|
"name": "Permit Join (60s)",
|
||||||
"x": 170,
|
"x": 170,
|
||||||
"y": 500,
|
"y": 640,
|
||||||
"group": "uig1ba3cd58",
|
"group": "uig93b85217",
|
||||||
"payload": "{\"value\": true, \"time\": 60}",
|
"payload": "{\"value\": true, \"time\": 60}",
|
||||||
"payloadType": "json",
|
"payloadType": "json",
|
||||||
"topic": "zigbee2mqtt/bridge/request/permit_join",
|
"topic": "zigbee2mqtt/bridge/request/permit_join",
|
||||||
"width": "3",
|
"width": "3",
|
||||||
"height": "1",
|
"height": "1",
|
||||||
"order": 1,
|
"order": 5,
|
||||||
"className": ""
|
"className": "",
|
||||||
|
"z": "fd235da06",
|
||||||
|
"icon": "",
|
||||||
|
"label": "Permit Join (60s)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "mqtt-out",
|
"id": "mqtt-out",
|
||||||
"z": "f11689cf1",
|
|
||||||
"type": "mqtt out",
|
"type": "mqtt out",
|
||||||
"wires": [],
|
"wires": [],
|
||||||
"name": "",
|
"name": "",
|
||||||
"x": 770,
|
"x": 770,
|
||||||
"y": 640,
|
"y": 780,
|
||||||
"broker": "br9dfef2b2",
|
"broker": "braff51339",
|
||||||
"topic": "",
|
"topic": "",
|
||||||
"qos": "2",
|
"qos": "2",
|
||||||
"retain": false
|
"retain": false,
|
||||||
|
"z": "fd235da06"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
+55
-3
@@ -3,7 +3,6 @@ import future.keywords.if
|
|||||||
import future.keywords.contains
|
import future.keywords.contains
|
||||||
import future.keywords.in
|
import future.keywords.in
|
||||||
|
|
||||||
# helpers
|
|
||||||
config_node(typ) if typ in {"mqtt-broker", "ui-base", "ui-page", "ui-group"}
|
config_node(typ) if typ in {"mqtt-broker", "ui-base", "ui-page", "ui-group"}
|
||||||
welcome_isolated(typ) if typ in {"debug", "mqtt-broker", "ui-base", "ui-page", "ui-group", "ui-text", "ui-chart", "ui-gauge", "ui-button", "ui-switch", "ui-slider", "inject", "comment", "mqtt in", "catch", "status", "link in"}
|
welcome_isolated(typ) if typ in {"debug", "mqtt-broker", "ui-base", "ui-page", "ui-group", "ui-text", "ui-chart", "ui-gauge", "ui-button", "ui-switch", "ui-slider", "inject", "comment", "mqtt in", "catch", "status", "link in"}
|
||||||
|
|
||||||
@@ -31,8 +30,6 @@ has_outbound_wire(source_id) if {
|
|||||||
count(node.wires) > 0
|
count(node.wires) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- DENY RULES ----
|
|
||||||
|
|
||||||
deny_missing_type contains msg if {
|
deny_missing_type contains msg if {
|
||||||
some node in input
|
some node in input
|
||||||
not node.type
|
not node.type
|
||||||
@@ -175,3 +172,58 @@ deny_broker_port contains msg if {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is_valid_port(p) if { p >= 1; p <= 65535 }
|
is_valid_port(p) if { p >= 1; p <= 65535 }
|
||||||
|
|
||||||
|
# Check that all flow nodes have wires field
|
||||||
|
deny_missing_wires contains msg if {
|
||||||
|
some node in input
|
||||||
|
node.type in {"mqtt in", "mqtt out", "function", "debug", "inject", "ui-text", "ui-button", "change", "switch", "delay", "exec"}
|
||||||
|
not "wires" in object.keys(node)
|
||||||
|
msg := sprintf("node %s (%s): missing wires field", [node.id, node.type])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check mqtt-broker has autoConnect = true
|
||||||
|
deny_no_autoconnect contains msg if {
|
||||||
|
some node in input
|
||||||
|
node.type == "mqtt-broker"
|
||||||
|
not node.autoConnect
|
||||||
|
msg := sprintf("node %s (mqtt-broker): autoConnect must be true", [node.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_protocol_version(v) if {
|
||||||
|
some p in {"3", "4", "5"}
|
||||||
|
v == p
|
||||||
|
}
|
||||||
|
|
||||||
|
deny_invalid_protocol contains msg if {
|
||||||
|
some node in input
|
||||||
|
node.type == "mqtt-broker"
|
||||||
|
node.protocolVersion
|
||||||
|
not valid_protocol_version(node.protocolVersion)
|
||||||
|
msg := sprintf("node %s (mqtt-broker): invalid protocolVersion %s", [node.id, node.protocolVersion])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that broker has at least one user
|
||||||
|
deny_unused_broker contains msg if {
|
||||||
|
some broker in input
|
||||||
|
broker.type == "mqtt-broker"
|
||||||
|
not has_broker_user(broker.id)
|
||||||
|
msg := sprintf("node %s (mqtt-broker): no mqtt in/out nodes reference this broker", [broker.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
has_broker_user(bid) if {
|
||||||
|
some node in input
|
||||||
|
node.broker == bid
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# nodered 5.0 bug: tabs with internal `nodes` array break flow loading.
|
||||||
|
# Use FLAT structure: all nodes at top level with z field referencing tab id.
|
||||||
|
deny_wrapped_tab contains msg if {
|
||||||
|
some tab in input
|
||||||
|
tab.type == "tab"
|
||||||
|
tab.nodes
|
||||||
|
count(tab.nodes) > 0
|
||||||
|
msg := sprintf("tab %s has internal nodes array (use flat structure - all nodes at top level with z field)", [tab.id])
|
||||||
|
}
|
||||||
|
|||||||
+14
-1
@@ -40,7 +40,7 @@ def make_broker_node(name, cfg):
|
|||||||
def make_node(nd, col, row, broker_map, group_map):
|
def make_node(nd, col, row, broker_map, group_map):
|
||||||
ntype = nd["type"]
|
ntype = nd["type"]
|
||||||
nid = nd.get("id", new_id())
|
nid = nd.get("id", new_id())
|
||||||
node = {"id": nid, "z": FLOW_ID, "type": ntype, "wires": [],
|
node = {"id": nid, "type": ntype, "wires": [],
|
||||||
"name": nd.get("label", nd.get("name", ""))}
|
"name": nd.get("label", nd.get("name", ""))}
|
||||||
node["x"] = COLUMNS[col] if col < len(COLUMNS) else col * 300 + 170
|
node["x"] = COLUMNS[col] if col < len(COLUMNS) else col * 300 + 170
|
||||||
node["y"] = row * ROW_HEIGHT + 80
|
node["y"] = row * ROW_HEIGHT + 80
|
||||||
@@ -145,9 +145,22 @@ def build_flow(doc, flow_def, node_positions):
|
|||||||
})
|
})
|
||||||
group_map[grp["name"]] = gid
|
group_map[grp["name"]] = gid
|
||||||
|
|
||||||
|
nodes.append({
|
||||||
|
"id": FLOW_ID,
|
||||||
|
"type": "tab",
|
||||||
|
"label": flow_def.get("tab", "Flow"),
|
||||||
|
"disabled": False,
|
||||||
|
"info": flow_def.get("description", ""),
|
||||||
|
"env": [],
|
||||||
|
})
|
||||||
|
|
||||||
for i, nd in enumerate(flow_def["nodes"]):
|
for i, nd in enumerate(flow_def["nodes"]):
|
||||||
col, row = node_positions.get(nd["id"], (0, i))
|
col, row = node_positions.get(nd["id"], (0, i))
|
||||||
node = make_node(nd, col, row, broker_map, group_map)
|
node = make_node(nd, col, row, broker_map, group_map)
|
||||||
|
node["z"] = FLOW_ID
|
||||||
|
if node.get("type") == "ui-button":
|
||||||
|
node["icon"] = ""
|
||||||
|
node["label"] = nd.get("label", nd.get("name", ""))
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
|
||||||
wires = flow_def.get("wires", {})
|
wires = flow_def.get("wires", {})
|
||||||
|
|||||||
Reference in New Issue
Block a user