#!/usr/bin/env python3 """Convert declarative flow YAML to Node-RED flow JSON (Dashboard 2.0).""" import json import sys import uuid import yaml COLUMNS = [170, 450, 770] ROW_HEIGHT = 140 BROKER_DEFAULTS = { "clientid": "", "autoConnect": True, "usetls": False, "protocolVersion": "4", "keepalive": "60", "cleansession": True, "birthTopic": "", "birthQos": "0", "birthPayload": "", "closeTopic": "", "closeQos": "0", "closePayload": "", "willTopic": "", "willQos": "0", "willPayload": "", } THEME_DEFAULTS = { "colors": {"surface": "#ffffff", "primary": "#0094CE", "bgPage": "#eeeeee", "groupBg": "#ffffff", "groupOutline": "#cccccc"}, "sizes": {"pagePadding": "12px", "groupGap": "12px", "groupBorderRadius": "4px", "widgetGap": "12px"}, } def new_id(prefix="n"): return prefix + uuid.uuid4().hex[:8] 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)) node.update(BROKER_DEFAULTS) return node def make_node(nd, col, row, broker_map, group_map): ntype = nd["type"] nid = nd.get("id", new_id()) node = {"id": nid, "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-in", "mqtt out", "mqtt-out"): node["broker"] = broker_map[nd["broker"]] if ntype in ("mqtt in", "mqtt-in"): node["topic"] = nd["topic"] node["qos"] = str(nd.get("qos", 2)) node["datatype"] = "auto" if ntype in ("mqtt out", "mqtt-out"): node["topic"] = nd.get("topic", "") node["qos"] = str(nd.get("qos", 2)) 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["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["width"] = "3" node["height"] = "1" node["order"] = nd.get("order", 1) node["className"] = "" if ntype == "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 build_flow(doc, flow_def, node_positions): nodes = [] broker_map = {} for nd in flow_def["nodes"]: 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: bn = make_broker_node(bname, doc["brokers"][bname]) broker_map[bname] = bn["id"] nodes.append(bn) group_map = {} 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 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"]): col, row = node_positions.get(nd["id"], (0, i)) 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) wires = flow_def.get("wires", {}) for src_id, targets in wires.items(): 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): col_order = {"mqtt in": 0, "ui-button": 0, "inject": 0, "debug": 2, "ui-text": 2, "mqtt out": 2} positions = {} for i, nd in enumerate(flow_def["nodes"]): col = col_order.get(nd["type"], 1) positions[nd["id"]] = (col, i // 3) return positions def main(): doc = yaml.safe_load(open(sys.argv[1])) all_nodes = [] for flow_def in doc.get("flows", []): positions = auto_position(flow_def) all_nodes += build_flow(doc, flow_def, positions) json.dump(all_nodes, sys.stdout, indent=2) if __name__ == "__main__": main()