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:
2026-06-30 22:12:22 -07:00
parent ca1c7eda26
commit 517f3dbdae
4 changed files with 458 additions and 199 deletions
+239 -75
View File
@@ -1,7 +1,6 @@
[
{
"id": "br1f38755c",
"z": "",
"id": "br9dfef2b2",
"type": "mqtt-broker",
"name": "mosquitto",
"broker": "mosquitto",
@@ -23,110 +22,275 @@
"willPayload": ""
},
{
"id": "t66ef1944",
"z": "",
"type": "ui_tab",
"id": "uibba9906e7",
"type": "ui-base",
"name": "Dashboard",
"path": "/dashboard",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"navigationStyle": "default",
"titleBarStyle": "default"
},
{
"id": "uitcdb20b5e",
"type": "ui-theme",
"name": "Default Theme",
"colors": {
"surface": "#ffffff",
"primary": "#0094CE",
"bgPage": "#eeeeee",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
},
"sizes": {
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "4px",
"widgetGap": "12px"
}
},
{
"id": "uipb062fd21",
"type": "ui-page",
"name": "Home",
"icon": "dashboard",
"order": 1,
"disabled": false,
"hidden": false
"ui": "uibba9906e7",
"icon": "home",
"path": "/home",
"theme": "uitcdb20b5e",
"layout": "flex",
"order": 0,
"className": "",
"visible": true,
"disabled": false
},
{
"id": "gc40a46a5",
"z": "t66ef1944",
"type": "ui_group",
"name": "Zigbee",
"tab": "t66ef1944",
"id": "uig1ba3cd58",
"type": "ui-group",
"name": "Plugs",
"page": "uipb062fd21",
"width": "6",
"height": "1",
"order": 1,
"disp": true,
"width": "6"
"showTitle": true,
"className": "",
"visible": true,
"disabled": false
},
{
"id": "zigbee-in",
"z": "f44444498",
"type": "mqtt-in",
"name": "",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"datatype": "auto",
"x": 170,
"y": 80,
"broker": "br1f38755c",
"topic": "zigbee2mqtt/#",
"qos": "2",
"id": "zigbee-state-squiggle",
"z": "f11689cf1",
"type": "mqtt in",
"wires": [
[
"debug",
"last-msg"
"text-squiggle"
]
]
],
"name": "",
"x": 170,
"y": 80,
"broker": "br9dfef2b2",
"topic": "zigbee2mqtt/Squiggle",
"qos": "0",
"datatype": "auto"
},
{
"id": "debug",
"z": "f44444498",
"type": "debug",
"name": "Debug",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"id": "zigbee-state-sideboard",
"z": "f11689cf1",
"type": "mqtt in",
"wires": [
[
"text-sideboard"
]
],
"name": "",
"x": 170,
"y": 80,
"broker": "br9dfef2b2",
"topic": "zigbee2mqtt/Sideboard Lamp",
"qos": "0",
"datatype": "auto"
},
{
"id": "zigbee-state-bamboo",
"z": "f11689cf1",
"type": "mqtt in",
"wires": [
[
"text-bamboo"
]
],
"name": "",
"x": 170,
"y": 80,
"broker": "br9dfef2b2",
"topic": "zigbee2mqtt/Bamboo Lights",
"qos": "0",
"datatype": "auto"
},
{
"id": "button-in",
"z": "f11689cf1",
"type": "mqtt in",
"wires": [
[
"btn-filter"
]
],
"name": "",
"x": 170,
"y": 220,
"broker": "br9dfef2b2",
"topic": "zigbee2mqtt/Door Button",
"qos": "0",
"datatype": "auto"
},
{
"id": "btn-filter",
"z": "f11689cf1",
"type": "function",
"wires": [
[
"plug-out-squiggle",
"plug-out-sideboard",
"plug-out-bamboo"
]
],
"name": "Single press only",
"x": 450,
"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",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": []
},
{
"id": "plug-out-squiggle",
"z": "f11689cf1",
"type": "mqtt out",
"wires": [],
"name": "",
"x": 770,
"y": 80
"y": 220,
"broker": "br9dfef2b2",
"topic": "zigbee2mqtt/Squiggle/set",
"qos": "2",
"retain": false
},
{
"id": "last-msg",
"z": "f44444498",
"id": "plug-out-sideboard",
"z": "f11689cf1",
"type": "mqtt out",
"wires": [],
"name": "",
"x": 770,
"y": 360,
"broker": "br9dfef2b2",
"topic": "zigbee2mqtt/Sideboard Lamp/set",
"qos": "2",
"retain": false
},
{
"id": "plug-out-bamboo",
"z": "f11689cf1",
"type": "mqtt out",
"wires": [],
"name": "",
"x": 770,
"y": 360,
"broker": "br9dfef2b2",
"topic": "zigbee2mqtt/Bamboo Lights/set",
"qos": "2",
"retain": false
},
{
"id": "text-squiggle",
"z": "f11689cf1",
"type": "ui-text",
"name": "Text",
"wires": [],
"name": "Squiggle",
"x": 770,
"y": 360,
"group": "uig1ba3cd58",
"width": "6",
"height": "2",
"order": 1,
"label": "Squiggle",
"format": "{{msg.payload.state}}",
"layout": "row-spread",
"className": ""
},
{
"id": "text-sideboard",
"z": "f11689cf1",
"type": "ui-text",
"wires": [],
"name": "Sideboard Lamp",
"x": 770,
"y": 80,
"group": "gc40a46a5",
"format": "{{msg.payload|json}}"
"y": 500,
"group": "uig1ba3cd58",
"width": "6",
"height": "2",
"order": 1,
"label": "Sideboard Lamp",
"format": "{{msg.payload.state}}",
"layout": "row-spread",
"className": ""
},
{
"id": "text-bamboo",
"z": "f11689cf1",
"type": "ui-text",
"wires": [],
"name": "Bamboo Lights",
"x": 770,
"y": 500,
"group": "uig1ba3cd58",
"width": "6",
"height": "2",
"order": 1,
"label": "Bamboo Lights",
"format": "{{msg.payload.state}}",
"layout": "row-spread",
"className": ""
},
{
"id": "permit-btn",
"z": "f44444498",
"z": "f11689cf1",
"type": "ui-button",
"name": "Permit Join (60s)",
"width": "3",
"height": "1",
"payloadType": "json",
"x": 170,
"y": 220,
"group": "gc40a46a5",
"payload": "{\"value\": true, \"time\": 60}",
"topic": "zigbee2mqtt/bridge/request/permit_join",
"wires": [
[
"mqtt-out"
]
]
],
"name": "Permit Join (60s)",
"x": 170,
"y": 500,
"group": "uig1ba3cd58",
"payload": "{\"value\": true, \"time\": 60}",
"payloadType": "json",
"topic": "zigbee2mqtt/bridge/request/permit_join",
"width": "3",
"height": "1",
"order": 1,
"className": ""
},
{
"id": "mqtt-out",
"z": "f44444498",
"type": "mqtt-out",
"z": "f11689cf1",
"type": "mqtt out",
"wires": [],
"name": "",
"qos": "2",
"retain": false,
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"x": 770,
"y": 220,
"broker": "br1f38755c",
"topic": ""
"y": 640,
"broker": "br9dfef2b2",
"topic": "",
"qos": "2",
"retain": false
}
]