Files
xilica-node-red-integration/flows.json
2025-11-06 10:54:51 +00:00

1068 lines
33 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[
{
"id": "b475cb7c37295110",
"type": "tab",
"label": "Xilica FR1 Dashboard",
"disabled": false,
"info": ""
},
{
"id": "42887eae2f29d3f0",
"type": "tab",
"label": "Xilica FR1 Dashboard",
"disabled": false,
"info": ""
},
{
"id": "ui_base_main",
"type": "ui-base",
"name": "Main UI",
"path": "/ui",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-control",
"ui-notification"
],
"showPathInSidebar": false,
"headerContent": "dashboard",
"titleBarStyle": "fixed",
"showReconnectNotification": true,
"notificationDisplayTime": 5,
"showDisconnectNotification": true,
"allowInstall": true
},
{
"id": "efd7cd1777464c3d",
"type": "ui-theme",
"name": "LAV THEME",
"colors": {
"surface": "#d7d6d6",
"primary": "#5c727a",
"bgPage": "#eeeeee",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
},
"sizes": {
"density": "default",
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "4px",
"widgetGap": "12px"
}
},
{
"id": "ui_page_solaro",
"type": "ui-page",
"name": "Solaro FR1",
"ui": "ui_base_main",
"path": "",
"icon": "volume-high",
"layout": "grid",
"theme": "efd7cd1777464c3d",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "3"
},
{
"name": "Tablet",
"px": "576",
"cols": "6"
},
{
"name": "Small Desktop",
"px": "768",
"cols": "9"
},
{
"name": "Desktop",
"px": "1024",
"cols": "12"
}
],
"order": 1,
"className": "",
"visible": true,
"disabled": false
},
{
"id": "ui_group_master",
"type": "ui-group",
"name": "Master Gain",
"page": "ui_page_solaro",
"width": "4",
"height": "8",
"order": 2,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "959e129052ea58f8",
"type": "ui-page",
"name": "Network",
"ui": "ui_base_main",
"path": "/network",
"icon": "wifi",
"layout": "grid",
"theme": "efd7cd1777464c3d",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "3"
},
{
"name": "Tablet",
"px": "576",
"cols": "6"
},
{
"name": "Small Desktop",
"px": "768",
"cols": "9"
},
{
"name": "Desktop",
"px": "1024",
"cols": "12"
}
],
"order": 2,
"className": "",
"visible": true,
"disabled": false
},
{
"id": "e46886701344918d",
"type": "global-config",
"env": []
},
{
"id": "c1fc81e5a697e224",
"type": "ui-group",
"name": "Wi-Fi",
"page": "959e129052ea58f8",
"width": "11",
"height": "11",
"order": 1,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "e3b7d0b0ef7a4a01",
"type": "ui-page",
"name": "Solaro Channels",
"ui": "ui_base_main",
"path": "/channels",
"icon": "view-dashboard-outline",
"layout": "tabs",
"theme": "efd7cd1777464c3d",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "3"
},
{
"name": "Tablet",
"px": "576",
"cols": "6"
},
{
"name": "Small Desktop",
"px": "768",
"cols": "9"
},
{
"name": "Desktop",
"px": "1024",
"cols": "12"
}
],
"order": 3,
"className": "",
"visible": "true",
"disabled": "false"
},
{
"id": "e7bb2d8c0e0e2312",
"type": "ui-group",
"name": "Full Range",
"page": "e3b7d0b0ef7a4a01",
"width": "12",
"height": "8",
"order": 2,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "ee3561b1b7a5a913",
"type": "ui-group",
"name": "Subwoofers",
"page": "e3b7d0b0ef7a4a01",
"width": "12",
"height": "8",
"order": 3,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "6f58b0a32d6db114",
"type": "ui-group",
"name": "Right Side",
"page": "e3b7d0b0ef7a4a01",
"width": "4",
"height": "8",
"order": 4,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "d0b6f2a2e4f2c211",
"type": "ui-group",
"name": "Left Side",
"page": "e3b7d0b0ef7a4a01",
"width": "4",
"height": "8",
"order": 1,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "ab164b2f3eebf7d2",
"type": "ui-group",
"name": "Meters",
"page": "ui_page_solaro",
"width": "6",
"height": "6",
"order": 1,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "99314ffcd1e195f5",
"type": "ui-text",
"z": "42887eae2f29d3f0",
"group": "ui_group_master",
"order": 2,
"width": "",
"height": "",
"name": "Current (dB)",
"label": "Current (dB)",
"layout": "row-spread",
"style": false,
"font": "",
"fontSize": "",
"color": "#000000",
"wrapText": false,
"className": "",
"value": "payload",
"valueType": "msg",
"x": 1430,
"y": 280,
"wires": []
},
{
"id": "b6e3985ddcab4c91",
"type": "ui-slider",
"z": "42887eae2f29d3f0",
"group": "ui_group_master",
"name": "MASTER_GAIN (dB)",
"label": "MASTER_GAIN (dB)",
"order": 1,
"width": "2",
"height": "8",
"passthru": false,
"outs": "all",
"topic": "",
"topicType": "str",
"thumbLabel": "true",
"showTicks": "false",
"min": -100,
"max": -18,
"step": 0.5,
"className": "",
"iconPrepend": "",
"iconAppend": "",
"color": "grey",
"colorTrack": "blue",
"colorThumb": "black",
"showTextField": false,
"x": 200,
"y": 300,
"wires": [
[
"2979ab7ac78ee6fb"
]
]
},
{
"id": "2979ab7ac78ee6fb",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "build SET MASTER_GAIN <v>",
"func": "const v = Number(msg.payload);\n// show value on the slider label while sending\nnode.status({text: v.toFixed(1)+\" dB\"});\nmsg.payload = `SET MASTER_GAIN ${v}`;\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 450,
"y": 300,
"wires": [
[
"f2a2b33c99f73ef4"
]
]
},
{
"id": "deb330478d545760",
"type": "inject",
"z": "42887eae2f29d3f0",
"name": "poll GET",
"props": [
{
"p": "payload"
}
],
"repeat": "0.1",
"crontab": "",
"once": true,
"onceDelay": "0.5",
"topic": "",
"payload": "GET MASTER_GAIN",
"payloadType": "str",
"x": 120,
"y": 360,
"wires": [
[
"f2a2b33c99f73ef4"
]
]
},
{
"id": "f2a2b33c99f73ef4",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "append \\r",
"func": "if (typeof msg.payload !== 'string') msg.payload = String(msg.payload);\nmsg.payload += \"\\r\";\nreturn msg;",
"outputs": 1,
"x": 680,
"y": 340,
"wires": [
[
"b025382cef8ddeae"
]
]
},
{
"id": "b025382cef8ddeae",
"type": "tcp request",
"z": "42887eae2f29d3f0",
"name": "FR1 192.168.1.244:10007",
"server": "192.168.1.244",
"port": "10007",
"out": "sit",
"ret": "string",
"splitc": " ",
"newline": "",
"trim": false,
"tls": "",
"x": 910,
"y": 400,
"wires": [
[
"d8273409d003853b",
"eee5fc7d794d0a80",
"eaf29f8c14a08046",
"93a6e10d2495c341"
]
]
},
{
"id": "eee5fc7d794d0a80",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "parse GET reply → number",
"func": "// Expected: MASTER_GAIN=-6.0\\r or OK\\r after SET\nconst s = (msg.payload||\"\").trim();\nif (/^MASTER_GAIN\\s*=/.test(s)) {\n const m = s.match(/=\\s*([-+]?\\d+(?:\\.\\d+)?)/);\n if (m) {\n const val = Number(m[1]);\n // update text and slider\n node.send([{payload: val.toFixed(1)}, {payload: val}]);\n }\n}\nreturn null; // outputs handled via node.send",
"outputs": 2,
"x": 1220,
"y": 320,
"wires": [
[
"99314ffcd1e195f5"
],
[
"56f258d24535e171"
]
]
},
{
"id": "d8273409d003853b",
"type": "debug",
"z": "42887eae2f29d3f0",
"name": "Raw replies",
"active": false,
"tosidebar": true,
"complete": "payload",
"statusVal": "",
"statusType": "auto",
"x": 1170,
"y": 360,
"wires": []
},
{
"id": "56f258d24535e171",
"type": "link out",
"z": "42887eae2f29d3f0",
"name": "link out 2",
"mode": "link",
"links": [
"95de5752c6e3da4b"
],
"x": 1385,
"y": 340,
"wires": []
},
{
"id": "95de5752c6e3da4b",
"type": "link in",
"z": "42887eae2f29d3f0",
"name": "link in 2",
"links": [
"56f258d24535e171"
],
"x": 55,
"y": 300,
"wires": [
[
"b6e3985ddcab4c91"
]
]
},
{
"id": "739b4b01aa486c67",
"type": "inject",
"z": "42887eae2f29d3f0",
"name": "poll VU_MASTER",
"props": [
{
"p": "payload"
}
],
"repeat": "0.2",
"crontab": "",
"once": true,
"onceDelay": "0.5",
"topic": "",
"payload": "GET VU_MASTER",
"payloadType": "str",
"x": 150,
"y": 400,
"wires": [
[
"749a672579e69106"
]
]
},
{
"id": "eaf29f8c14a08046",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "parse VU_MASTER replies",
"func": "const s = String(msg.payload || '').trim();\nconst m = s.match(/^VU[_\\s]?MASTER\\s*=\\s*([-+]?\\d+(?:\\.\\d+)?)/i);\nif (!m) return null;\nmsg.payload = Number(m[1]); // e.g. -28.7\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1220,
"y": 400,
"wires": [
[
"f7d60274be119ba9"
]
]
},
{
"id": "749a672579e69106",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "append \\r",
"func": "if (typeof msg.payload !== 'string') msg.payload = String(msg.payload);\nmsg.payload += \"\\r\";\nreturn msg;",
"outputs": 1,
"x": 320,
"y": 400,
"wires": [
[
"b025382cef8ddeae"
]
]
},
{
"id": "22cd982ac31a676b",
"type": "debug",
"z": "42887eae2f29d3f0",
"name": "Template OUT",
"active": false,
"tosidebar": true,
"complete": "payload",
"statusVal": "",
"statusType": "auto",
"x": 1600,
"y": 400,
"wires": []
},
{
"id": "f7d60274be119ba9",
"type": "ui-template",
"z": "42887eae2f29d3f0",
"group": "ab164b2f3eebf7d2",
"page": "",
"ui": "",
"name": "vu_master",
"order": 1,
"width": "2",
"height": "8",
"head": "",
"format": "<template>\n <div class=\"vu-vertical\">\n <div class=\"vu-hdr\">\n <strong>VU_MASTER</strong>\n <span class=\"vu-readout\">{{ level.toFixed(1) }} dB</span>\n </div>\n\n <div class=\"vu-bar\">\n <div class=\"vu-fill\" :style=\"{ height: percent + '%' }\"></div>\n <div class=\"vu-peak\" :style=\"{ bottom: peakPercent + '%' }\"></div>\n <span class=\"vu-tick\" v-for=\"t in ticks\" :key=\"t\" :style=\"{ bottom: t + '%' }\"></span>\n </div>\n\n <div class=\"vu-ftr\">\n <span>{{ max }} dB</span>\n <span>3</span>\n <span>12</span>\n <span>{{ min }} dB</span>\n </div>\n </div>\n</template>\n\n<script>\nexport default {\n props: ['msg'],\n data: () => ({\n min: -60,\n max: 0,\n level: -60,\n peak: -60,\n decayDbPerSec: 6,\n _timer: null,\n ticks: [0, 80, 95, 100]\n }),\n computed: {\n percent () {\n const v = Math.min(this.max, Math.max(this.min, this.level));\n return ((v - this.min) / (this.max - this.min)) * 100;\n },\n peakPercent () {\n const v = Math.min(this.max, Math.max(this.min, this.peak));\n return ((v - this.min) / (this.max - this.min)) * 100;\n }\n },\n watch: {\n msg: {\n deep: true,\n immediate: true,\n handler (m) {\n if (!m) return;\n if (m.min !== undefined && Number.isFinite(+m.min)) this.min = +m.min;\n if (m.max !== undefined && Number.isFinite(+m.max)) this.max = +m.max;\n if (m.payload !== undefined) {\n const n = Number(m.payload);\n if (Number.isFinite(n)) {\n this.level = n;\n if (n > this.peak) this.peak = n;\n }\n }\n }\n }\n },\n mounted () {\n const dt = 100;\n this._timer = setInterval(() => {\n const step = this.decayDbPerSec * (dt / 1000);\n if (this.peak > this.level) this.peak = Math.max(this.level, this.peak - step);\n else this.peak = this.level;\n }, dt);\n },\n unmounted () {\n if (this._timer) clearInterval(this._timer);\n }\n}\n</script>\n\n<style>\n.vu-vertical {\n width: 100px;\n height: 450px;\n display: flex;\n flex-direction: column;\n align-items: center;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n}\n\n.vu-hdr, .vu-ftr {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n}\n\n.vu-readout {\n font-variant-numeric: tabular-nums;\n opacity: .8;\n}\n\n.vu-bar {\n position: relative;\n flex: 1;\n width: 30px;\n margin: 6px 0;\n border-radius: 4px;\n background: #0b0f14;\n overflow: hidden;\n box-shadow: inset 0 0 0 1px rgba(255,255,255,.06);\n display: flex;\n flex-direction: column-reverse;\n}\n\n.vu-fill {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n z-index: 1;\n transition: height 90ms linear;\n background: linear-gradient(0deg,\n #10b981 0%, #10b981 80%, /* green up to -12 dB */\n #f59e0b 80%, #f59e0b 95%, /* amber to -3 dB */\n #ef4444 95%, #ef4444 100%); /* red above -3 dB */\n}\n\n.vu-peak {\n position: absolute;\n left: 0;\n right: 0;\n height: 2px;\n background: rgba(255,255,255,.9);\n box-shadow: 0 0 4px rgba(255,255,255,.9);\n z-index: 2;\n}\n\n.vu-tick {\n position: absolute;\n left: 0;\n right: 0;\n height: 1px;\n background: rgba(255,255,255,.15);\n z-index: 0;\n}\n\n.vu-ftr span {\n font-size: 12px;\n color: rgba(255,255,255,.6);\n}\n</style>\n",
"storeOutMessages": true,
"passthru": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 1430,
"y": 400,
"wires": [
[
"22cd982ac31a676b"
]
]
},
{
"id": "98fcc2746ef99725",
"type": "inject",
"z": "42887eae2f29d3f0",
"name": "Poll CH4/5/6 (200ms)",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "0.5",
"topic": "",
"payload": "",
"payloadType": "str",
"x": 160,
"y": 520,
"wires": [
[
"831afc4f6e643a54"
]
]
},
{
"id": "831afc4f6e643a54",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "Round-robin GET CH4/5/6",
"func": "// Build a single multi-line SUBSCRIBE message for CH4OUTCH28OUT\n// Each subscription is sent via TCP to Solaro\n\nconst lines = [];\n\n// Set update rate to 100ms\nlines.push('INTERVAL 100');\n\n// Generate all channel names dynamically (CH4OUT → CH28OUT)\nfor (let i = 4; i <= 28; i++) {\n lines.push(`SUBSCRIBE CH${i}OUT \"TCP\"`);\n}\nfor (let i = 1; i <= 4; i++) {\n lines.push(`SUBSCRIBE CH${i}_FROUT \"TCP\"`);\n}\nfor (let i = 25; i <= 28; i++) {\n lines.push(`SUBSCRIBE CH${i}_FROUT \"TCP\"`);\n}\nfor (let i = 1; i <= 4; i++) {\n lines.push(`SUBSCRIBE CH${i}_SUBOUT \"TCP\"`);\n}\nfor (let i = 25; i <= 28; i++) {\n lines.push(`SUBSCRIBE CH${i}_SUBOUT \"TCP\"`);\n}\n// Join commands with CR and append final CR (required by Solaro API)\nmsg.payload = lines.join('\\r') + '\\r';\n\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 400,
"y": 520,
"wires": [
[
"037d562b5e1e1b78"
]
]
},
{
"id": "037d562b5e1e1b78",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "append \\r",
"func": "if (typeof msg.payload !== 'string') msg.payload = String(msg.payload);\nmsg.payload += \"\\r\";\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 520,
"wires": [
[
"e5b7326c3b0a69e3",
"b025382cef8ddeae"
]
]
},
{
"id": "59dbb335713fd5f0",
"type": "rbe",
"z": "42887eae2f29d3f0",
"name": "Only on change per channel",
"func": "rbe",
"gap": "",
"start": "",
"inout": "out",
"septopics": true,
"property": "payload.state",
"topi": "topic",
"x": 960,
"y": 580,
"wires": [
[
"a7281d90be3924f1"
]
]
},
{
"id": "c728f8b711b33b49",
"type": "udp out",
"z": "42887eae2f29d3f0",
"name": "OSC → CueCore2 10.2.71.130:8000",
"addr": "10.2.71.130",
"iface": "",
"port": "8000",
"ipv": "udp4",
"outport": "",
"base64": false,
"x": 1630,
"y": 580,
"wires": []
},
{
"id": "a7281d90be3924f1",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "Encode OSC /position/<idx> int <state>",
"func": "// Remember which channel was last ON\nlet lastOn = context.get('lastOn') || null;\n\nconst { idx, state } = msg.payload;\n\n// Only act when something changes\nif (state === 1) {\n // If a new channel turns ON\n if (lastOn !== idx) {\n // Update context\n context.set('lastOn', idx);\n\n // Encode OSC message for this channel\n function pad4(buf) {\n const pad = (4 - (buf.length % 4)) % 4;\n return Buffer.concat([buf, Buffer.alloc(pad)]);\n }\n\n function oscString(s) {\n return pad4(\n Buffer.concat([\n Buffer.from(s, 'utf8'),\n Buffer.from([0])\n ])\n );\n }\n\n const addr = oscString(`/position/${idx}`);\n const types = oscString(',i');\n const arg = Buffer.alloc(4);\n arg.writeInt32BE(1, 0); // ON\n\n msg.payload = Buffer.concat([addr, types, arg]);\n node.status({ text: `ON → position ${idx}` });\n return msg;\n } else {\n // Same channel turned on again → ignore\n return null;\n }\n} else if (state === 0 && lastOn === idx) {\n // If the currently active one turned off → reset\n context.set('lastOn', null);\n node.status({ text: `OFF → position ${idx}` });\n}\n\nreturn null;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1260,
"y": 580,
"wires": [
[
"e72e32cf5af0e00f",
"c728f8b711b33b49"
]
]
},
{
"id": "f24442d61d44086a",
"type": "debug",
"z": "42887eae2f29d3f0",
"name": "Parsed {idx,state}",
"active": true,
"tosidebar": true,
"complete": "payload",
"statusVal": "",
"statusType": "auto",
"x": 1550,
"y": 480,
"wires": []
},
{
"id": "e72e32cf5af0e00f",
"type": "debug",
"z": "42887eae2f29d3f0",
"name": "OSC bytes",
"active": true,
"tosidebar": true,
"complete": "payload",
"x": 1570,
"y": 520,
"wires": []
},
{
"id": "e5b7326c3b0a69e3",
"type": "debug",
"z": "42887eae2f29d3f0",
"name": "FR1 raw",
"active": true,
"tosidebar": true,
"complete": "payload",
"x": 620,
"y": 460,
"wires": []
},
{
"id": "93a6e10d2495c341",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "Parse CHxOUT=TRUE/0 → {idx,state}",
"func": "const s = String(msg.payload ?? '').trim();\n\n// Accepts #CH12OUT, #CH1_FROUT, #CH28_SUBOUT\nconst m = s.match(/^(#CH((?:\\d+)(?:_(?:FR|SUB))?)OUT)\\s*=\\s*(.*)$/i);\nif (!m) return null;\n\nconst name = m[1];\nconst raw = m[3];\n\n// Get the leading digits after #CH, even if there's _FR/_SUB\nconst idxMatch = name.match(/^#CH(\\d+)/i);\nconst idx = idxMatch ? Number(idxMatch[1]) : NaN;\n\nlet state;\nif (/^(TRUE|on)$/i.test(raw)) {\n state = 0;\n} else if (/^(FALSE|off)$/i.test(raw)) {\n state = 1;\n} else if (/^[-+]?\\d+(?:\\.\\d+)?$/.test(raw)) {\n state = Number(raw) !== 0 ? 1 : 0;\n} else {\n state = 0;\n}\n\nreturn {\n topic: name.toUpperCase(),\n payload: { idx, state }\n};\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1250,
"y": 480,
"wires": [
[
"f24442d61d44086a",
"59dbb335713fd5f0"
]
]
},
{
"id": "689ea5b3b7e5b6fb",
"type": "ui-button",
"z": "42887eae2f29d3f0",
"group": "c1fc81e5a697e224",
"name": "Scan Wi-Fi",
"label": "Scan Wi-Fi",
"order": 1,
"width": "1",
"height": "1",
"emulateClick": false,
"className": "",
"icon": "",
"iconPosition": "left",
"payload": "",
"payloadType": "str",
"topic": "",
"topicType": "str",
"buttonColor": "",
"textColor": "",
"iconColor": "",
"enableClick": true,
"enablePointerdown": false,
"pointerdownPayload": "",
"pointerdownPayloadType": "str",
"enablePointerup": false,
"pointerupPayload": "",
"pointerupPayloadType": "str",
"x": 130,
"y": 740,
"wires": [
[
"e5a955dbcbd0b0f9"
]
]
},
{
"id": "e5a955dbcbd0b0f9",
"type": "exec",
"z": "42887eae2f29d3f0",
"command": "bash",
"addpay": false,
"append": "-lc \"sudo -n nmcli -t -f SSID,SIGNAL,SECURITY dev wifi rescan >/dev/null 2>&1; sudo -n nmcli -t -f SSID,SIGNAL,SECURITY dev wifi list\"",
"useSpawn": "false",
"timer": "",
"winHide": false,
"name": "nmcli scan",
"x": 360,
"y": 750,
"wires": [
[
"c5625882bf1d9523",
"650fa075c7bd5ee4"
],
[],
[
"70623f49cca4d94e"
]
]
},
{
"id": "c5625882bf1d9523",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "parse nmcli → options",
"func": "// Input (stdout): \"SSID:SIGNAL:SECURITY\" lines\nconst lines = String(msg.payload || \"\")\n .split(/\\r?\\n/)\n .map(s => s.trim())\n .filter(Boolean);\n\nconst bestBySsid = new Map();\nfor (const line of lines) {\n const parts = line.split(\":\");\n const ssid = parts[0] || \"\"; // may be empty (hidden)\n const signal = Number(parts[1] || 0);\n const sec = parts.slice(2).join(\":\"); // SECURITY may contain colons\n\n const display = ssid || \"(hidden)\";\n const label = `${display} (${signal}%)${sec ? \" • \" + sec : \"\"}`;\n\n const prev = bestBySsid.get(ssid);\n if (!prev || signal > prev.signal) {\n bestBySsid.set(ssid, { label, value: ssid, signal });\n }\n}\n\nmsg.options = [...bestBySsid.values()].map(({ label, value }) => ({ label, value }));\nmsg.payload = null; // keep payload clean\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 570,
"y": 750,
"wires": [
[
"4c38affd4fc6f7fd"
]
]
},
{
"id": "4c38affd4fc6f7fd",
"type": "ui-dropdown",
"z": "42887eae2f29d3f0",
"group": "c1fc81e5a697e224",
"name": "SSID",
"label": "SSID",
"tooltip": "",
"order": 2,
"width": 3,
"height": 1,
"passthru": true,
"multiple": false,
"chips": false,
"clearable": false,
"options": [],
"topic": "",
"topicType": "str",
"className": "",
"typeIsComboBox": true,
"msgTrigger": "onChange",
"x": 770,
"y": 750,
"wires": [
[
"9143c8d7cb5f0695"
]
]
},
{
"id": "9143c8d7cb5f0695",
"type": "change",
"z": "42887eae2f29d3f0",
"name": "store ssid",
"rules": [
{
"t": "set",
"p": "ssid",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"x": 950,
"y": 750,
"wires": [
[]
]
},
{
"id": "5d220b230ade6d6d",
"type": "ui-text-input",
"z": "42887eae2f29d3f0",
"group": "c1fc81e5a697e224",
"name": "Password",
"label": "Password",
"order": 3,
"width": 3,
"height": 1,
"topic": "",
"mode": "password",
"delay": "0",
"className": "",
"x": 1000,
"y": 800,
"wires": [
[
"1011308429ae7b27"
]
]
},
{
"id": "1011308429ae7b27",
"type": "change",
"z": "42887eae2f29d3f0",
"name": "store pass (flow)",
"rules": [
{
"t": "set",
"p": "pass",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"x": 1200,
"y": 800,
"wires": [
[]
]
},
{
"id": "814b0a944c467115",
"type": "ui-button",
"z": "42887eae2f29d3f0",
"group": "c1fc81e5a697e224",
"name": "Connect",
"label": "Connect",
"order": 4,
"width": "1",
"height": "1",
"emulateClick": false,
"className": "",
"icon": "",
"iconPosition": "left",
"payload": "",
"payloadType": "str",
"topic": "",
"topicType": "str",
"buttonColor": "",
"textColor": "",
"iconColor": "",
"enableClick": true,
"enablePointerdown": false,
"pointerdownPayload": "",
"pointerdownPayloadType": "str",
"enablePointerup": false,
"pointerupPayload": "",
"pointerupPayloadType": "str",
"x": 120,
"y": 900,
"wires": [
[
"7a4b562cba69efaa"
]
]
},
{
"id": "7a4b562cba69efaa",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "build nmcli cmd",
"func": "const ssid = flow.get('ssid');\nconst pass = flow.get('pass') || '';\nif (!ssid) return [null, { payload: 'Select an SSID first.' }];\n\nconst q = s => `'` + String(s).replace(/'/g, `'\\\\''`) + `'`; // safe single-quote\n\nlet inner = `/usr/bin/nmcli dev wifi connect ${q(ssid)}`;\nif (pass) inner += ` password ${q(pass)}`;\n\n// Exec node runs: sh -c \"<inner with sudo>\"\nconst cmd = `-c \"sudo -n ${inner}\"`;\n\nreturn [\n { payload: cmd }, // → Exec (runs: sh -c \"sudo -n /usr/bin/nmcli …\")\n { payload: `Connecting to ${ssid}...` } // → Status\n];\n",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 330,
"y": 900,
"wires": [
[
"9f7621962705882b"
],
[
"f84f8a46913a2fef"
]
]
},
{
"id": "9f7621962705882b",
"type": "exec",
"z": "42887eae2f29d3f0",
"command": "sh",
"addpay": "payload",
"append": "",
"useSpawn": "false",
"timer": "",
"winHide": false,
"name": "nmcli connect",
"x": 640,
"y": 860,
"wires": [
[
"c066f2a161e02498"
],
[],
[
"70623f49cca4d94e"
]
]
},
{
"id": "c066f2a161e02498",
"type": "function",
"z": "42887eae2f29d3f0",
"name": "result → status",
"func": "const s = String(msg.payload||'');\nlet ok = /successfully activated|Device.*successfully activated/i.test(s);\nlet txt = ok ? 'Connected.' : s.trim() || 'Failed.';\nreturn { payload: txt };\n",
"outputs": 1,
"x": 710,
"y": 1000,
"wires": [
[
"f84f8a46913a2fef"
]
]
},
{
"id": "f84f8a46913a2fef",
"type": "ui-text",
"z": "42887eae2f29d3f0",
"group": "c1fc81e5a697e224",
"order": 5,
"width": 6,
"height": 1,
"name": "Status",
"label": "Status",
"layout": "row-spread",
"style": false,
"font": "",
"fontSize": "",
"color": "#000000",
"wrapText": true,
"className": "",
"value": "payload",
"valueType": "msg",
"x": 870,
"y": 960,
"wires": []
},
{
"id": "70623f49cca4d94e",
"type": "debug",
"z": "42887eae2f29d3f0",
"name": "stderr",
"active": false,
"tosidebar": true,
"complete": "payload",
"x": 810,
"y": 800,
"wires": []
},
{
"id": "650fa075c7bd5ee4",
"type": "debug",
"z": "42887eae2f29d3f0",
"name": "nmcli scan debug",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 590,
"y": 660,
"wires": []
}
]