Add multi-plug toggle, debounce via unique topic subscriptions
Button (Door Button) now toggles Squiggle, Sideboard Lamp, and Bamboo Lights
via three parallel mqtt-out publishes.
Key fix: avoid subscribing to a wildcard '#' topic alongside a specific topic
in the same Node-RED instance. The mqtt-broker re-adds its handler as an
EventEmitter listener on each subscribe call (file:
/usr/src/node-red/node_modules/@node-red/nodes/core/network/10-mqtt.js
_clientOn('message', subscription.handler) is unconditional), so after
each deploy/restart the same message arrives N times to internal listeners.
Each mqtt-in node now uses a unique topic (zigbee2mqtt/<device>).
Removed zigbee-in # wildcard and plug-state ui-text widgets (state now
displayed via per-plug ui-text widgets).
Added function node support to flow2json.py for msg filtering logic.
Added ui-text format/width/height defaults support.
Added dashboard 2.0 config node defaults matching reference flow:
ui-base, ui-theme, ui-page, ui-group with full property set.
This commit is contained in:
+99
-80
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert declarative flow YAML to Node-RED flow JSON."""
|
||||
"""Convert declarative flow YAML to Node-RED flow JSON (Dashboard 2.0)."""
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
@@ -14,26 +14,11 @@ BROKER_DEFAULTS = {
|
||||
"closeTopic": "", "closeQos": "0", "closePayload": "",
|
||||
"willTopic": "", "willQos": "0", "willPayload": "",
|
||||
}
|
||||
|
||||
NODE_TEMPLATES = {
|
||||
"mqtt-in": {
|
||||
"nl": False, "rap": True, "rh": 0, "inputs": 0, "datatype": "auto",
|
||||
},
|
||||
"mqtt-out": {
|
||||
"qos": "2", "retain": False, "respTopic": "", "contentType": "",
|
||||
"userProps": "", "correl": "", "expiry": "",
|
||||
},
|
||||
"debug": {
|
||||
"active": True, "tosidebar": True, "console": False,
|
||||
"tostatus": False, "complete": "payload", "targetType": "msg",
|
||||
"statusVal": "", "statusType": "auto",
|
||||
},
|
||||
"ui-text": {
|
||||
"width": "6", "height": "2", "layout": "row-spread",
|
||||
},
|
||||
"ui-button": {
|
||||
"width": "3", "height": "1", "payloadType": "json",
|
||||
},
|
||||
THEME_DEFAULTS = {
|
||||
"colors": {"surface": "#ffffff", "primary": "#0094CE", "bgPage": "#eeeeee",
|
||||
"groupBg": "#ffffff", "groupOutline": "#cccccc"},
|
||||
"sizes": {"pagePadding": "12px", "groupGap": "12px",
|
||||
"groupBorderRadius": "4px", "widgetGap": "12px"},
|
||||
}
|
||||
|
||||
|
||||
@@ -41,110 +26,144 @@ def new_id(prefix="n"):
|
||||
return prefix + uuid.uuid4().hex[:8]
|
||||
|
||||
|
||||
def make_broker_node(name, cfg, flow_id):
|
||||
node = {"id": new_id("br"), "z": "", "type": "mqtt-broker", "name": name}
|
||||
FLOW_ID = new_id("f")
|
||||
|
||||
|
||||
def make_broker_node(name, cfg):
|
||||
node = {"id": new_id("br"), "type": "mqtt-broker", "name": name}
|
||||
node["broker"] = cfg["host"]
|
||||
node["port"] = str(cfg.get("port", 1883))
|
||||
for k, v in BROKER_DEFAULTS.items():
|
||||
node.setdefault(k, v)
|
||||
node.update(BROKER_DEFAULTS)
|
||||
return node
|
||||
|
||||
|
||||
def make_node(nd, col, row, broker_map, group_map, flow_id):
|
||||
def make_node(nd, col, row, broker_map, group_map):
|
||||
ntype = nd["type"]
|
||||
nid = nd["id"] if "id" in nd else new_id()
|
||||
node = {"id": nid, "z": flow_id, "type": ntype, "name": nd.get("label", nd.get("name", ""))}
|
||||
node.update(NODE_TEMPLATES.get(ntype, {}))
|
||||
nid = nd.get("id", new_id())
|
||||
node = {"id": nid, "z": FLOW_ID, "type": ntype, "wires": [],
|
||||
"name": nd.get("label", nd.get("name", ""))}
|
||||
node["x"] = COLUMNS[col] if col < len(COLUMNS) else col * 300 + 170
|
||||
node["y"] = row * ROW_HEIGHT + 80
|
||||
|
||||
if ntype in ("mqtt-in", "mqtt-out"):
|
||||
if ntype in ("mqtt in", "mqtt-in", "mqtt out", "mqtt-out"):
|
||||
node["broker"] = broker_map[nd["broker"]]
|
||||
if ntype in ("ui-text", "ui-button"):
|
||||
node["group"] = group_map[nd["group"]]
|
||||
if ntype == "mqtt-in":
|
||||
if ntype in ("mqtt in", "mqtt-in"):
|
||||
node["topic"] = nd["topic"]
|
||||
node["qos"] = str(nd.get("qos", 2))
|
||||
if ntype == "mqtt-out":
|
||||
node["datatype"] = "auto"
|
||||
if ntype in ("mqtt out", "mqtt-out"):
|
||||
node["topic"] = nd.get("topic", "")
|
||||
node["qos"] = str(nd.get("qos", 2))
|
||||
if ntype == "ui-text":
|
||||
node["retain"] = False
|
||||
if ntype in ("ui-text", "ui-button"):
|
||||
node["group"] = group_map[nd["group"]]
|
||||
if node["type"] in ("ui-text",):
|
||||
node["width"] = nd.get("width", "6")
|
||||
node["height"] = nd.get("height", "2")
|
||||
node["order"] = nd.get("order", 1)
|
||||
node["label"] = nd.get("label", node.get("name", ""))
|
||||
node["format"] = nd.get("format", "{{msg.payload}}")
|
||||
node["name"] = nd.get("label", nd.get("name", "Text"))
|
||||
if ntype == "ui-button":
|
||||
node["layout"] = nd.get("layout", "row-spread")
|
||||
node["className"] = ""
|
||||
if node["type"] in ("ui-button",):
|
||||
node["payload"] = json.dumps(nd.get("payload", ""))
|
||||
node["payloadType"] = "json"
|
||||
node["topic"] = nd.get("topic", "")
|
||||
node["name"] = nd.get("label", nd.get("name", "Button"))
|
||||
node["width"] = "3"
|
||||
node["height"] = "1"
|
||||
node["order"] = nd.get("order", 1)
|
||||
node["className"] = ""
|
||||
if ntype == "debug":
|
||||
node["name"] = nd.get("label", nd.get("name", "Debug"))
|
||||
|
||||
node["name"] = nd.get("label", "Debug")
|
||||
node["active"] = True
|
||||
node["tosidebar"] = True
|
||||
node["complete"] = "payload"
|
||||
if ntype == "function":
|
||||
node["name"] = nd.get("label", nd.get("name", "Function"))
|
||||
node["func"] = nd["code"]
|
||||
node["outputs"] = nd.get("outputs", 1)
|
||||
node["noerr"] = 0
|
||||
node["initialize"] = ""
|
||||
node["finalize"] = ""
|
||||
node["libs"] = []
|
||||
return node
|
||||
|
||||
|
||||
def make_group_node(name, tab_id, order):
|
||||
return {
|
||||
"id": new_id("g"), "z": tab_id, "type": "ui_group",
|
||||
"name": name, "tab": tab_id, "order": order,
|
||||
"disp": True, "width": "6",
|
||||
}
|
||||
|
||||
|
||||
def make_tab_node(name, icon, order):
|
||||
tab_id = new_id("t")
|
||||
return tab_id, {
|
||||
"id": tab_id, "z": "", "type": "ui_tab",
|
||||
"name": name, "icon": icon, "order": order,
|
||||
"disabled": False, "hidden": False,
|
||||
}
|
||||
|
||||
|
||||
def build_flow(doc, flow_def, node_positions):
|
||||
nodes = []
|
||||
flow_id = new_id("f")
|
||||
|
||||
# Collect broker references
|
||||
broker_map = {}
|
||||
for nd in flow_def["nodes"]:
|
||||
if nd["type"] in ("mqtt-in", "mqtt-out") and "broker" in nd:
|
||||
if nd["type"] in ("mqtt in", "mqtt-in", "mqtt out", "mqtt-out") and "broker" in nd:
|
||||
bname = nd["broker"]
|
||||
if bname not in broker_map:
|
||||
bcfg = doc["brokers"][bname]
|
||||
bn = make_broker_node(bname, bcfg, flow_id)
|
||||
bn = make_broker_node(bname, doc["brokers"][bname])
|
||||
broker_map[bname] = bn["id"]
|
||||
nodes.append(bn)
|
||||
|
||||
# Collect dashboard groups/tabs
|
||||
group_map = {}
|
||||
if "dashboards" in doc:
|
||||
for db in doc["dashboards"]:
|
||||
tab_id, tn = make_tab_node(db["tab"], db.get("icon", "dashboard"), db.get("order", 1))
|
||||
nodes.append(tn)
|
||||
for gi, grp in enumerate(db.get("groups", [])):
|
||||
gn = make_group_node(grp["name"], tab_id, gi + 1)
|
||||
group_map[grp["name"]] = gn["id"]
|
||||
nodes.append(gn)
|
||||
for db in doc.get("dashboards", []):
|
||||
bid = new_id("uib")
|
||||
tid = new_id("uit")
|
||||
nodes.append({
|
||||
"id": bid, "type": "ui-base", "name": db["base"],
|
||||
"path": db.get("path", "/dashboard"),
|
||||
"includeClientData": True,
|
||||
"acceptsClientConfig": ["ui-notification", "ui-control"],
|
||||
"showPathInSidebar": False,
|
||||
"navigationStyle": "default",
|
||||
"titleBarStyle": "default",
|
||||
})
|
||||
nodes.append({
|
||||
"id": tid, "type": "ui-theme",
|
||||
"name": db.get("theme", "Default Theme"),
|
||||
"colors": db.get("themeColors", THEME_DEFAULTS["colors"]),
|
||||
"sizes": db.get("themeSizes", THEME_DEFAULTS["sizes"]),
|
||||
})
|
||||
for pi, page in enumerate(db.get("pages", [])):
|
||||
pid = new_id("uip")
|
||||
nodes.append({
|
||||
"id": pid, "type": "ui-page",
|
||||
"name": page["page"], "ui": bid,
|
||||
"icon": page.get("icon", "home"),
|
||||
"path": page.get("path", "/home"),
|
||||
"theme": page.get("theme", tid),
|
||||
"layout": page.get("layout", "flex"),
|
||||
"order": pi,
|
||||
"className": "", "visible": True, "disabled": False,
|
||||
})
|
||||
for gi, grp in enumerate(page.get("groups", [])):
|
||||
gid = new_id("uig")
|
||||
nodes.append({
|
||||
"id": gid, "type": "ui-group",
|
||||
"name": grp["name"], "page": pid,
|
||||
"width": str(grp.get("width", 6)),
|
||||
"height": "1",
|
||||
"order": gi + 1,
|
||||
"showTitle": True,
|
||||
"className": "", "visible": True, "disabled": False,
|
||||
})
|
||||
group_map[grp["name"]] = gid
|
||||
|
||||
# Create flow nodes with auto-positioning
|
||||
for i, nd in enumerate(flow_def["nodes"]):
|
||||
col, row = node_positions.get(nd["id"], (0, i))
|
||||
node = make_node(nd, col, row, broker_map, group_map, flow_id)
|
||||
node = make_node(nd, col, row, broker_map, group_map)
|
||||
nodes.append(node)
|
||||
|
||||
# Wire connections
|
||||
wires = flow_def.get("wires", {})
|
||||
for src_id, targets in wires.items():
|
||||
src = next(n for n in nodes if n.get("id") == src_id or n.get("name", "").lower().replace(" ", "-") == src_id)
|
||||
src.setdefault("wires", [])
|
||||
src["wires"].append(targets)
|
||||
src = next((n for n in nodes if n["id"] == src_id), None)
|
||||
if src:
|
||||
src.setdefault("wires", [])
|
||||
src["wires"].append(targets)
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def auto_position(flow_def):
|
||||
"""Simple auto-layout: put each node in a column determined by its role."""
|
||||
col_order = {"mqtt in": 0, "ui-button": 0, "inject": 0,
|
||||
"debug": 2, "ui-text": 2, "mqtt out": 2}
|
||||
positions = {}
|
||||
col_order = {"mqtt-in": 0, "ui-button": 0, "inject": 0,
|
||||
"debug": 2, "ui-text": 2, "mqtt-out": 2, "ui-chart": 2, "ui-gauge": 2}
|
||||
for i, nd in enumerate(flow_def["nodes"]):
|
||||
col = col_order.get(nd["type"], 1)
|
||||
positions[nd["id"]] = (col, i // 3)
|
||||
|
||||
Reference in New Issue
Block a user