[
    {
        "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 CH4OUT–CH28OUT\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": []
    }
]