@@ -19,3 +19,6 @@ infra/nodered/.config.nodes.json
|
|||||||
infra/nodered/.config.runtime.json
|
infra/nodered/.config.runtime.json
|
||||||
infra/nodered/.config.users.json
|
infra/nodered/.config.users.json
|
||||||
infra/nodered/flows_cred.json
|
infra/nodered/flows_cred.json
|
||||||
|
|
||||||
|
# operator tool pin
|
||||||
|
mise.toml
|
||||||
|
|||||||
+90
-37
@@ -5,6 +5,16 @@ brokers:
|
|||||||
host: mosquitto
|
host: mosquitto
|
||||||
port: 1883
|
port: 1883
|
||||||
|
|
||||||
|
# Plugs toggled on brief single press
|
||||||
|
single_plugs:
|
||||||
|
- Squiggle
|
||||||
|
- Sideboard Lamp
|
||||||
|
- Bamboo Lights
|
||||||
|
|
||||||
|
# Plugs toggled on press-hold-release (action: "release" after a "hold")
|
||||||
|
hold_plugs:
|
||||||
|
- DJ Booth
|
||||||
|
|
||||||
dashboards:
|
dashboards:
|
||||||
- base: Dashboard
|
- base: Dashboard
|
||||||
pages:
|
pages:
|
||||||
@@ -17,35 +27,32 @@ 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: button-in
|
- id: button-in
|
||||||
type: mqtt in
|
type: mqtt in
|
||||||
broker: mosquitto
|
broker: mosquitto
|
||||||
@@ -56,10 +63,6 @@ flows:
|
|||||||
type: function
|
type: function
|
||||||
name: Single click
|
name: Single click
|
||||||
code: |
|
code: |
|
||||||
const now = Date.now();
|
|
||||||
const last = context.get('lastTime') || 0;
|
|
||||||
if (now - last < 50) return null;
|
|
||||||
context.set('lastTime', now);
|
|
||||||
try {
|
try {
|
||||||
const p = JSON.parse(msg.payload);
|
const p = JSON.parse(msg.payload);
|
||||||
if (p.action === 'single') {
|
if (p.action === 'single') {
|
||||||
@@ -73,10 +76,6 @@ flows:
|
|||||||
type: function
|
type: function
|
||||||
name: Hold release
|
name: Hold release
|
||||||
code: |
|
code: |
|
||||||
const now = Date.now();
|
|
||||||
const last = context.get('lastTime') || 0;
|
|
||||||
if (now - last < 50) return null;
|
|
||||||
context.set('lastTime', now);
|
|
||||||
try {
|
try {
|
||||||
const p = JSON.parse(msg.payload);
|
const p = JSON.parse(msg.payload);
|
||||||
if (p.action === 'release') {
|
if (p.action === 'release') {
|
||||||
@@ -86,6 +85,54 @@ 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: 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: permit-btn
|
- id: permit-btn
|
||||||
type: ui-button
|
type: ui-button
|
||||||
group: Plugs
|
group: Plugs
|
||||||
@@ -94,12 +141,18 @@ flows:
|
|||||||
payload:
|
payload:
|
||||||
value: true
|
value: true
|
||||||
time: 60
|
time: 60
|
||||||
order: 9
|
order: 5
|
||||||
|
|
||||||
- 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]
|
||||||
button-in: [single-filter, hold-filter]
|
button-in: [single-filter, hold-filter]
|
||||||
|
single-filter: [plug-out-squiggle, plug-out-sideboard, plug-out-bamboo]
|
||||||
|
hold-filter: [plug-out-djbooth]
|
||||||
permit-btn: [mqtt-out]
|
permit-btn: [mqtt-out]
|
||||||
|
|||||||
Regular → Executable
@@ -2,6 +2,7 @@ listener 1883
|
|||||||
allow_anonymous true
|
allow_anonymous true
|
||||||
|
|
||||||
acl_file /mosquitto/config/acl.conf
|
acl_file /mosquitto/config/acl.conf
|
||||||
|
password_file /mosquitto/config/passwd
|
||||||
|
|
||||||
persistence true
|
persistence true
|
||||||
persistence_location /mosquitto/data
|
persistence_location /mosquitto/data
|
||||||
|
|||||||
@@ -0,0 +1,440 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "brd05c91e3",
|
||||||
|
"type": "mqtt-broker",
|
||||||
|
"name": "mosquitto",
|
||||||
|
"broker": "mosquitto",
|
||||||
|
"port": "1883",
|
||||||
|
"clientid": "",
|
||||||
|
"autoConnect": true,
|
||||||
|
"usetls": false,
|
||||||
|
"protocolVersion": "4",
|
||||||
|
"keepalive": "60",
|
||||||
|
"cleansession": true,
|
||||||
|
"birthTopic": "",
|
||||||
|
"birthQos": "0",
|
||||||
|
"birthPayload": "",
|
||||||
|
"closeTopic": "",
|
||||||
|
"closeQos": "0",
|
||||||
|
"closePayload": "",
|
||||||
|
"willTopic": "",
|
||||||
|
"willQos": "0",
|
||||||
|
"willPayload": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "uib31315d0b",
|
||||||
|
"type": "ui-base",
|
||||||
|
"name": "Dashboard",
|
||||||
|
"path": "/dashboard",
|
||||||
|
"includeClientData": true,
|
||||||
|
"acceptsClientConfig": [
|
||||||
|
"ui-notification",
|
||||||
|
"ui-control"
|
||||||
|
],
|
||||||
|
"showPathInSidebar": false,
|
||||||
|
"navigationStyle": "default",
|
||||||
|
"titleBarStyle": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "uit1f2d1d17",
|
||||||
|
"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": "uipc7a69acb",
|
||||||
|
"type": "ui-page",
|
||||||
|
"name": "Home",
|
||||||
|
"ui": "uib31315d0b",
|
||||||
|
"icon": "home",
|
||||||
|
"path": "/home",
|
||||||
|
"theme": "uit1f2d1d17",
|
||||||
|
"layout": "flex",
|
||||||
|
"order": 0,
|
||||||
|
"className": "",
|
||||||
|
"visible": true,
|
||||||
|
"disabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "uig32ecccd2",
|
||||||
|
"type": "ui-group",
|
||||||
|
"name": "Plugs",
|
||||||
|
"page": "uipc7a69acb",
|
||||||
|
"width": "6",
|
||||||
|
"height": "1",
|
||||||
|
"order": 1,
|
||||||
|
"showTitle": true,
|
||||||
|
"className": "",
|
||||||
|
"visible": true,
|
||||||
|
"disabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f810d3244",
|
||||||
|
"type": "tab",
|
||||||
|
"label": "Zigbee Monitor",
|
||||||
|
"disabled": false,
|
||||||
|
"info": "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.",
|
||||||
|
"env": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zigbee-state-squiggle",
|
||||||
|
"type": "mqtt in",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"text-squiggle"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/Squiggle",
|
||||||
|
"qos": "0",
|
||||||
|
"datatype": "auto",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 170,
|
||||||
|
"y": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-squiggle",
|
||||||
|
"type": "ui-text",
|
||||||
|
"wires": [],
|
||||||
|
"name": "Squiggle",
|
||||||
|
"group": "uig32ecccd2",
|
||||||
|
"width": "6",
|
||||||
|
"height": "2",
|
||||||
|
"order": 1,
|
||||||
|
"label": "Squiggle",
|
||||||
|
"format": "{{msg.payload.state}}",
|
||||||
|
"layout": "row-spread",
|
||||||
|
"className": "",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plug-out-squiggle",
|
||||||
|
"type": "mqtt out",
|
||||||
|
"wires": [],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/Squiggle/set",
|
||||||
|
"qos": "2",
|
||||||
|
"retain": false,
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 220
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zigbee-state-sideboard",
|
||||||
|
"type": "mqtt in",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"text-sideboard"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/Sideboard Lamp",
|
||||||
|
"qos": "0",
|
||||||
|
"datatype": "auto",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 170,
|
||||||
|
"y": 290
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-sideboard",
|
||||||
|
"type": "ui-text",
|
||||||
|
"wires": [],
|
||||||
|
"name": "Sideboard Lamp",
|
||||||
|
"group": "uig32ecccd2",
|
||||||
|
"width": "6",
|
||||||
|
"height": "2",
|
||||||
|
"order": 2,
|
||||||
|
"label": "Sideboard Lamp",
|
||||||
|
"format": "{{msg.payload.state}}",
|
||||||
|
"layout": "row-spread",
|
||||||
|
"className": "",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 360
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plug-out-sideboard",
|
||||||
|
"type": "mqtt out",
|
||||||
|
"wires": [],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/Sideboard Lamp/set",
|
||||||
|
"qos": "2",
|
||||||
|
"retain": false,
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 430
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zigbee-state-bamboo",
|
||||||
|
"type": "mqtt in",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"text-bamboo"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/Bamboo Lights",
|
||||||
|
"qos": "0",
|
||||||
|
"datatype": "auto",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 170,
|
||||||
|
"y": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-bamboo",
|
||||||
|
"type": "ui-text",
|
||||||
|
"wires": [],
|
||||||
|
"name": "Bamboo Lights",
|
||||||
|
"group": "uig32ecccd2",
|
||||||
|
"width": "6",
|
||||||
|
"height": "2",
|
||||||
|
"order": 3,
|
||||||
|
"label": "Bamboo Lights",
|
||||||
|
"format": "{{msg.payload.state}}",
|
||||||
|
"layout": "row-spread",
|
||||||
|
"className": "",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 570
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plug-out-bamboo",
|
||||||
|
"type": "mqtt out",
|
||||||
|
"wires": [],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/Bamboo Lights/set",
|
||||||
|
"qos": "2",
|
||||||
|
"retain": false,
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 640
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zigbee-state-couch",
|
||||||
|
"type": "mqtt in",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"text-couch"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/Dual Outlet - Couch",
|
||||||
|
"qos": "0",
|
||||||
|
"datatype": "auto",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 170,
|
||||||
|
"y": 710
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-couch",
|
||||||
|
"type": "ui-text",
|
||||||
|
"wires": [],
|
||||||
|
"name": "Dual Outlet - Couch",
|
||||||
|
"group": "uig32ecccd2",
|
||||||
|
"width": "6",
|
||||||
|
"height": "2",
|
||||||
|
"order": 4,
|
||||||
|
"label": "Dual Outlet - Couch",
|
||||||
|
"format": "{{msg.payload.state_1}} / {{msg.payload.state_2}}",
|
||||||
|
"layout": "row-spread",
|
||||||
|
"className": "",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 780
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mapper-couch",
|
||||||
|
"type": "function",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"plug-out-couch"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "Toggle each outlet",
|
||||||
|
"func": "msg.payload = {state_1: 'TOGGLE', state_2: 'TOGGLE'};\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"libs": [],
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 450,
|
||||||
|
"y": 850
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plug-out-couch",
|
||||||
|
"type": "mqtt out",
|
||||||
|
"wires": [],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/Dual Outlet - Couch/set",
|
||||||
|
"qos": "2",
|
||||||
|
"retain": false,
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 920
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zigbee-state-djbooth",
|
||||||
|
"type": "mqtt in",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"text-djbooth"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/DJ Booth",
|
||||||
|
"qos": "0",
|
||||||
|
"datatype": "auto",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 170,
|
||||||
|
"y": 990
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "text-djbooth",
|
||||||
|
"type": "ui-text",
|
||||||
|
"wires": [],
|
||||||
|
"name": "DJ Booth",
|
||||||
|
"group": "uig32ecccd2",
|
||||||
|
"width": "6",
|
||||||
|
"height": "2",
|
||||||
|
"order": 5,
|
||||||
|
"label": "DJ Booth",
|
||||||
|
"format": "{{msg.payload.state}}",
|
||||||
|
"layout": "row-spread",
|
||||||
|
"className": "",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 1060
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plug-out-djbooth",
|
||||||
|
"type": "mqtt out",
|
||||||
|
"wires": [],
|
||||||
|
"name": "",
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/DJ Booth/set",
|
||||||
|
"qos": "2",
|
||||||
|
"retain": false,
|
||||||
|
"z": "f810d3244",
|
||||||
|
"x": 770,
|
||||||
|
"y": 1130
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "button-in",
|
||||||
|
"type": "mqtt in",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"single-filter",
|
||||||
|
"hold-filter"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "",
|
||||||
|
"x": 170,
|
||||||
|
"y": 80,
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "zigbee2mqtt/Door Button",
|
||||||
|
"qos": "0",
|
||||||
|
"datatype": "auto",
|
||||||
|
"z": "f810d3244"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "single-filter",
|
||||||
|
"type": "function",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"plug-out-squiggle",
|
||||||
|
"plug-out-sideboard",
|
||||||
|
"plug-out-bamboo",
|
||||||
|
"mapper-couch"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "Single click",
|
||||||
|
"x": 450,
|
||||||
|
"y": 80,
|
||||||
|
"func": "const now = Date.now();\nconst last = context.get('lastTime') || 0;\nif (now - last < 50) return null;\ncontext.set('lastTime', now);\ntry {\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": [],
|
||||||
|
"z": "f810d3244"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hold-filter",
|
||||||
|
"type": "function",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"plug-out-djbooth"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "Hold release",
|
||||||
|
"x": 450,
|
||||||
|
"y": 80,
|
||||||
|
"func": "const now = Date.now();\nconst last = context.get('lastTime') || 0;\nif (now - last < 50) return null;\ncontext.set('lastTime', now);\ntry {\n const p = JSON.parse(msg.payload);\n if (p.action === 'release') {\n msg.payload = { state: 'TOGGLE' };\n return msg;\n }\n} catch (e) {}\nreturn null;\n",
|
||||||
|
"outputs": 1,
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"libs": [],
|
||||||
|
"z": "f810d3244"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "permit-btn",
|
||||||
|
"type": "ui-button",
|
||||||
|
"wires": [
|
||||||
|
[
|
||||||
|
"mqtt-out"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"name": "Permit Join (60s)",
|
||||||
|
"x": 170,
|
||||||
|
"y": 220,
|
||||||
|
"group": "uig32ecccd2",
|
||||||
|
"payload": "{\"value\": true, \"time\": 60}",
|
||||||
|
"payloadType": "json",
|
||||||
|
"topic": "zigbee2mqtt/bridge/request/permit_join",
|
||||||
|
"width": "3",
|
||||||
|
"height": "1",
|
||||||
|
"order": 9,
|
||||||
|
"className": "",
|
||||||
|
"z": "f810d3244",
|
||||||
|
"icon": "",
|
||||||
|
"label": "Permit Join (60s)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mqtt-out",
|
||||||
|
"type": "mqtt out",
|
||||||
|
"wires": [],
|
||||||
|
"name": "",
|
||||||
|
"x": 770,
|
||||||
|
"y": 220,
|
||||||
|
"broker": "brd05c91e3",
|
||||||
|
"topic": "",
|
||||||
|
"qos": "2",
|
||||||
|
"retain": false,
|
||||||
|
"z": "f810d3244"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -36,28 +36,27 @@ advanced:
|
|||||||
- 25
|
- 25
|
||||||
- 93
|
- 93
|
||||||
- 235
|
- 235
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
enabled: true
|
enabled: true
|
||||||
port: 8080
|
port: 8080
|
||||||
permit_join: false
|
permit_join: false
|
||||||
groups:
|
groups:
|
||||||
"1":
|
'1':
|
||||||
friendly_name: all_plugs
|
friendly_name: all_plugs
|
||||||
devices:
|
devices:
|
||||||
- Squiggle
|
- Squiggle
|
||||||
- Sideboard Lamp
|
- Sideboard Lamp
|
||||||
- Bamboo Lights
|
- Bamboo Lights
|
||||||
devices:
|
devices:
|
||||||
"0xffffb40e0604fbd0":
|
'0xffffb40e0604fbd0':
|
||||||
friendly_name: Squiggle
|
friendly_name: Squiggle
|
||||||
"0xffffb40e06065536":
|
'0xffffb40e06065536':
|
||||||
friendly_name: Door Button
|
friendly_name: Door Button
|
||||||
"0xffffb40e0603c9e4":
|
'0xffffb40e0603c9e4':
|
||||||
friendly_name: Sideboard Lamp
|
friendly_name: Sideboard Lamp
|
||||||
"0xffffb40e06050af6":
|
'0xffffb40e06050af6':
|
||||||
friendly_name: Bamboo Lights
|
friendly_name: Bamboo Lights
|
||||||
"0xffffb40e0603ef11":
|
'0xffffb40e0603ef11':
|
||||||
friendly_name: DJ Booth
|
friendly_name: DJ Booth
|
||||||
"0x4ce175523f710000":
|
'0x4ce175523f710000':
|
||||||
friendly_name: Dual Outlet - Couch
|
friendly_name: Dual Outlet - Couch
|
||||||
|
|||||||
+1
-125
@@ -37,117 +37,6 @@ 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())
|
||||||
@@ -265,17 +154,6 @@ 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)
|
||||||
@@ -285,9 +163,7 @@ 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 = dict(flow_def.get("wires", {}))
|
wires = 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