generator: auto-expand declarative plug lists with per-channel mapper
Add build_plugs() to flow2json.py: a flow can declare a plugs: table
({id, name, channels?}) plus single_plugs/hold_plugs lists and the
generator emits all plumbing automatically — state-in node, /set out node,
dashboard widget, and (for multi-channel devices) a mapper function that
toggles each channel (state_1/state_2) instead of the ignored shared
{state:TOGGLE} payload.
Rewrite zigbee-monitor.yaml to the declarative form; Dual Outlet - Couch
is multi-channel (channels: [1,2]). Adding a plug is one line, no manual
node/wire duplication. Single press toggles 5 devices (couch via mapper
verified E2E ON/OFF), hold-release still DJ Booth only.
Refs: david/nr-flow-validator#2
This commit is contained in:
+29
-118
@@ -5,17 +5,6 @@ brokers:
|
|||||||
host: mosquitto
|
host: mosquitto
|
||||||
port: 1883
|
port: 1883
|
||||||
|
|
||||||
# Plugs toggled on brief single press
|
|
||||||
single_plugs:
|
|
||||||
- Squiggle
|
|
||||||
- Sideboard Lamp
|
|
||||||
- Bamboo Lights
|
|
||||||
- Dual Outlet - Couch
|
|
||||||
|
|
||||||
# Plugs toggled on press-hold-release (action: "release" after a "hold")
|
|
||||||
hold_plugs:
|
|
||||||
- DJ Booth
|
|
||||||
|
|
||||||
dashboards:
|
dashboards:
|
||||||
- base: Dashboard
|
- base: Dashboard
|
||||||
pages:
|
pages:
|
||||||
@@ -28,38 +17,35 @@ dashboards:
|
|||||||
|
|
||||||
flows:
|
flows:
|
||||||
- tab: Zigbee Monitor
|
- tab: Zigbee Monitor
|
||||||
description: Button dispatches single vs hold-release to different plug sets
|
description: Button dispatches single vs hold-release to different plug sets.
|
||||||
|
Plug plumbing (state-in, /set out, dashboard widget, per-channel mapper)
|
||||||
|
is generated by flow2json.py from the plugs lists below.
|
||||||
|
|
||||||
|
# Declarative plug set. channels:[] marks a multi-channel device, which gets
|
||||||
|
# a mapper that toggles each channel and a widget showing each state_N.
|
||||||
|
plugs:
|
||||||
|
- id: squiggle
|
||||||
|
name: Squiggle
|
||||||
|
- id: sideboard
|
||||||
|
name: Sideboard Lamp
|
||||||
|
- id: bamboo
|
||||||
|
name: Bamboo Lights
|
||||||
|
- id: djbooth
|
||||||
|
name: DJ Booth
|
||||||
|
- id: couch
|
||||||
|
name: Dual Outlet - Couch
|
||||||
|
channels: [1, 2]
|
||||||
|
|
||||||
|
# Device ids toggled on brief single press (driven by single_filter)
|
||||||
|
single_plugs: [squiggle, sideboard, bamboo, couch]
|
||||||
|
|
||||||
|
# Device ids toggled on press-hold-release (driven by hold_filter)
|
||||||
|
hold_plugs: [djbooth]
|
||||||
|
|
||||||
|
single_filter: single-filter
|
||||||
|
hold_filter: hold-filter
|
||||||
|
|
||||||
nodes:
|
nodes:
|
||||||
- id: zigbee-state-squiggle
|
|
||||||
type: mqtt in
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/Squiggle
|
|
||||||
qos: 0
|
|
||||||
|
|
||||||
- id: zigbee-state-sideboard
|
|
||||||
type: mqtt in
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/Sideboard Lamp
|
|
||||||
qos: 0
|
|
||||||
|
|
||||||
- id: zigbee-state-bamboo
|
|
||||||
type: mqtt in
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/Bamboo Lights
|
|
||||||
qos: 0
|
|
||||||
|
|
||||||
- id: zigbee-state-djbooth
|
|
||||||
type: mqtt in
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/DJ Booth
|
|
||||||
qos: 0
|
|
||||||
|
|
||||||
- id: zigbee-state-couch
|
|
||||||
type: mqtt in
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/Dual Outlet - Couch
|
|
||||||
qos: 0
|
|
||||||
|
|
||||||
- id: button-in
|
- id: button-in
|
||||||
type: mqtt in
|
type: mqtt in
|
||||||
broker: mosquitto
|
broker: mosquitto
|
||||||
@@ -100,73 +86,6 @@ flows:
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
- id: plug-out-squiggle
|
|
||||||
type: mqtt out
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/Squiggle/set
|
|
||||||
|
|
||||||
- id: plug-out-sideboard
|
|
||||||
type: mqtt out
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/Sideboard Lamp/set
|
|
||||||
|
|
||||||
- id: plug-out-bamboo
|
|
||||||
type: mqtt out
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/Bamboo Lights/set
|
|
||||||
|
|
||||||
- id: plug-out-djbooth
|
|
||||||
type: mqtt out
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/DJ Booth/set
|
|
||||||
|
|
||||||
- id: couch-mapper
|
|
||||||
type: function
|
|
||||||
name: Toggle both outlets
|
|
||||||
code: |
|
|
||||||
msg.payload = { state_1: 'TOGGLE', state_2: 'TOGGLE' };
|
|
||||||
return msg;
|
|
||||||
|
|
||||||
- id: plug-out-couch
|
|
||||||
type: mqtt out
|
|
||||||
broker: mosquitto
|
|
||||||
topic: zigbee2mqtt/Dual Outlet - Couch/set
|
|
||||||
|
|
||||||
- id: text-squiggle
|
|
||||||
type: ui-text
|
|
||||||
group: Plugs
|
|
||||||
label: Squiggle
|
|
||||||
format: "{{msg.payload.state}}"
|
|
||||||
order: 1
|
|
||||||
|
|
||||||
- id: text-sideboard
|
|
||||||
type: ui-text
|
|
||||||
group: Plugs
|
|
||||||
label: Sideboard Lamp
|
|
||||||
format: "{{msg.payload.state}}"
|
|
||||||
order: 2
|
|
||||||
|
|
||||||
- id: text-bamboo
|
|
||||||
type: ui-text
|
|
||||||
group: Plugs
|
|
||||||
label: Bamboo Lights
|
|
||||||
format: "{{msg.payload.state}}"
|
|
||||||
order: 3
|
|
||||||
|
|
||||||
- id: text-djbooth
|
|
||||||
type: ui-text
|
|
||||||
group: Plugs
|
|
||||||
label: DJ Booth
|
|
||||||
format: "{{msg.payload.state}}"
|
|
||||||
order: 4
|
|
||||||
|
|
||||||
- id: text-couch
|
|
||||||
type: ui-text
|
|
||||||
group: Plugs
|
|
||||||
label: Dual Outlet - Couch
|
|
||||||
format: "{{msg.payload.state_1}} / {{msg.payload.state_2}}"
|
|
||||||
order: 6
|
|
||||||
|
|
||||||
- id: permit-btn
|
- id: permit-btn
|
||||||
type: ui-button
|
type: ui-button
|
||||||
group: Plugs
|
group: Plugs
|
||||||
@@ -175,20 +94,12 @@ flows:
|
|||||||
payload:
|
payload:
|
||||||
value: true
|
value: true
|
||||||
time: 60
|
time: 60
|
||||||
order: 5
|
order: 9
|
||||||
|
|
||||||
- id: mqtt-out
|
- id: mqtt-out
|
||||||
type: mqtt out
|
type: mqtt out
|
||||||
broker: mosquitto
|
broker: mosquitto
|
||||||
|
|
||||||
wires:
|
wires:
|
||||||
zigbee-state-squiggle: [text-squiggle]
|
|
||||||
zigbee-state-sideboard: [text-sideboard]
|
|
||||||
zigbee-state-bamboo: [text-bamboo]
|
|
||||||
zigbee-state-djbooth: [text-djbooth]
|
|
||||||
zigbee-state-couch: [text-couch]
|
|
||||||
button-in: [single-filter, hold-filter]
|
button-in: [single-filter, hold-filter]
|
||||||
single-filter: [plug-out-squiggle, plug-out-sideboard, plug-out-bamboo, couch-mapper]
|
|
||||||
couch-mapper: [plug-out-couch]
|
|
||||||
hold-filter: [plug-out-djbooth]
|
|
||||||
permit-btn: [mqtt-out]
|
permit-btn: [mqtt-out]
|
||||||
|
|||||||
+125
-1
@@ -37,6 +37,117 @@ def make_broker_node(name, cfg):
|
|||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def build_plugs(flow_def, doc, broker_map, group_map):
|
||||||
|
"""Expand a declarative `plugs:` list into state-in/out/widget nodes and wiring.
|
||||||
|
|
||||||
|
Each plug entry: {id, name, channels?}.
|
||||||
|
- single channel: widget shows {{msg.payload.state}}
|
||||||
|
- multi-channel (channels: [1, 2]): a mapper function is emitted before the
|
||||||
|
plug-out so the device toggles per-channel, and the widget shows each state_N.
|
||||||
|
|
||||||
|
single_plugs / hold_plugs are lists of plug ids driven by the function nodes
|
||||||
|
named single_filter / hold_filter respectively.
|
||||||
|
"""
|
||||||
|
if "plugs" not in flow_def:
|
||||||
|
return [], {}
|
||||||
|
|
||||||
|
_flow_order["text-order-counter"] = 0
|
||||||
|
|
||||||
|
plug_cfg = {p["id"]: p for p in flow_def["plugs"]}
|
||||||
|
|
||||||
|
def state_keys(p):
|
||||||
|
return [int(k) for k in p.get("channels", [])] if p.get("channels") else []
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
wires = {}
|
||||||
|
|
||||||
|
def wire(src, target):
|
||||||
|
wires.setdefault(src, []).append(target)
|
||||||
|
|
||||||
|
def channel_mapper_id(pid):
|
||||||
|
return f"mapper-{pid}"
|
||||||
|
|
||||||
|
for key in ("single_plugs", "hold_plugs"):
|
||||||
|
filter_id = flow_def.get("single_filter" if key == "single_plugs" else "hold_filter")
|
||||||
|
if not filter_id:
|
||||||
|
raise ValueError(f"flow missing {'single_filter' if key == 'single_plugs' else 'hold_filter'} for {key}")
|
||||||
|
for pid in flow_def.get(key, []):
|
||||||
|
p = plug_cfg[pid]
|
||||||
|
name = p["name"]
|
||||||
|
keys = state_keys(p)
|
||||||
|
|
||||||
|
sid = f"zigbee-state-{pid}"
|
||||||
|
oid = f"plug-out-{pid}"
|
||||||
|
tid = f"text-{pid}"
|
||||||
|
|
||||||
|
nodes.append({
|
||||||
|
"id": sid, "type": "mqtt in", "wires": [], "name": "",
|
||||||
|
"broker": broker_map[_broker(flow_def, doc)],
|
||||||
|
"topic": f"zigbee2mqtt/{name}", "qos": "0", "datatype": "auto",
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt = "{{msg.payload.state}}"
|
||||||
|
if keys:
|
||||||
|
parts = [f"{{{{msg.payload.state_{k}}}}}" for k in keys]
|
||||||
|
fmt = " / ".join(parts)
|
||||||
|
nodes.append({
|
||||||
|
"id": tid, "type": "ui-text", "wires": [], "name": name,
|
||||||
|
"group": group_map[_group(flow_def)],
|
||||||
|
"width": "6", "height": "2", "order": _order(flow_def),
|
||||||
|
"label": name, "format": fmt, "layout": "row-spread", "className": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
target = oid
|
||||||
|
if keys:
|
||||||
|
mid = channel_mapper_id(pid)
|
||||||
|
code = ", ".join(f"state_{k}: 'TOGGLE'" for k in keys)
|
||||||
|
nodes.append({
|
||||||
|
"id": mid, "type": "function", "wires": [], "name": "Toggle each outlet",
|
||||||
|
"func": f"msg.payload = {{{code}}};\nreturn msg;",
|
||||||
|
"outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [],
|
||||||
|
})
|
||||||
|
wire(filter_id, mid)
|
||||||
|
wire(mid, oid)
|
||||||
|
else:
|
||||||
|
wire(filter_id, target)
|
||||||
|
|
||||||
|
nodes.append({
|
||||||
|
"id": oid, "type": "mqtt out", "wires": [], "name": "",
|
||||||
|
"broker": broker_map[_broker(flow_def, doc)],
|
||||||
|
"topic": f"zigbee2mqtt/{name}/set", "qos": "2", "retain": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
wire(sid, tid)
|
||||||
|
|
||||||
|
return nodes, wires
|
||||||
|
|
||||||
|
|
||||||
|
def _broker(flow_def, doc):
|
||||||
|
# broker used by generated plug nodes: first broker referenced by any node, else first defined
|
||||||
|
for nd in flow_def.get("nodes", []):
|
||||||
|
if nd.get("broker"):
|
||||||
|
return nd["broker"]
|
||||||
|
brokers = list(doc.get("brokers", {}))
|
||||||
|
if not brokers:
|
||||||
|
raise ValueError("no broker defined")
|
||||||
|
return brokers[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _group(flow_def):
|
||||||
|
for nd in flow_def.get("nodes", []):
|
||||||
|
if nd.get("group"):
|
||||||
|
return nd["group"]
|
||||||
|
raise ValueError("no dashboard group defined")
|
||||||
|
|
||||||
|
|
||||||
|
_flow_order = {"text-order-counter": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def _order(flow_def):
|
||||||
|
_flow_order["text-order-counter"] += 1
|
||||||
|
return _flow_order["text-order-counter"]
|
||||||
|
|
||||||
|
|
||||||
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())
|
||||||
@@ -154,6 +265,17 @@ def build_flow(doc, flow_def, node_positions):
|
|||||||
"env": [],
|
"env": [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
plug_nodes, plug_wires = build_plugs(flow_def, doc, broker_map, group_map)
|
||||||
|
renum = 0
|
||||||
|
for pn in plug_nodes:
|
||||||
|
pn["z"] = FLOW_ID
|
||||||
|
col = {"mqtt in": 0, "function": 1, "mqtt out": 2, "ui-text": 2}.get(
|
||||||
|
pn.get("type"), 0)
|
||||||
|
pn["x"] = COLUMNS[col]
|
||||||
|
pn["y"] = 80 + (renum * ROW_HEIGHT // 2)
|
||||||
|
renum += 1
|
||||||
|
nodes.append(pn)
|
||||||
|
|
||||||
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)
|
||||||
@@ -163,7 +285,9 @@ def build_flow(doc, flow_def, node_positions):
|
|||||||
node["label"] = nd.get("label", nd.get("name", ""))
|
node["label"] = nd.get("label", nd.get("name", ""))
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
|
||||||
wires = flow_def.get("wires", {})
|
wires = dict(flow_def.get("wires", {}))
|
||||||
|
for k, v in plug_wires.items():
|
||||||
|
wires.setdefault(k, []).extend(v)
|
||||||
for src_id, targets in wires.items():
|
for src_id, targets in wires.items():
|
||||||
src = next((n for n in nodes if n["id"] == src_id), None)
|
src = next((n for n in nodes if n["id"] == src_id), None)
|
||||||
if src:
|
if src:
|
||||||
|
|||||||
Reference in New Issue
Block a user