#!/usr/bin/env python3 """Convert declarative flow YAML to Node-RED flow JSON.""" 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": "", } 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", }, } 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} node["broker"] = cfg["host"] node["port"] = str(cfg.get("port", 1883)) for k, v in BROKER_DEFAULTS.items(): node.setdefault(k, v) return node def make_node(nd, col, row, broker_map, group_map, flow_id): 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, {})) 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"): node["broker"] = broker_map[nd["broker"]] if ntype in ("ui-text", "ui-button"): node["group"] = group_map[nd["group"]] if ntype == "mqtt-in": node["topic"] = nd["topic"] node["qos"] = str(nd.get("qos", 2)) if ntype == "mqtt-out": node["topic"] = nd.get("topic", "") node["qos"] = str(nd.get("qos", 2)) if ntype == "ui-text": node["format"] = nd.get("format", "{{msg.payload}}") node["name"] = nd.get("label", nd.get("name", "Text")) if ntype == "ui-button": node["payload"] = json.dumps(nd.get("payload", "")) node["topic"] = nd.get("topic", "") node["name"] = nd.get("label", nd.get("name", "Button")) if ntype == "debug": node["name"] = nd.get("label", nd.get("name", "Debug")) 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: bname = nd["broker"] if bname not in broker_map: bcfg = doc["brokers"][bname] bn = make_broker_node(bname, bcfg, flow_id) 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) # 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) 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) return nodes def auto_position(flow_def): """Simple auto-layout: put each node in a column determined by its role.""" 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) 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()