Node-RED Flow: Domoticz ↔ WLED Two-Way Sync with Dynamic Device Detection

Moderator: leecollings

Post Reply
drogert
Posts: 39
Joined: Saturday 26 April 2014 15:27
Target OS: Raspberry Pi / ODroid
Domoticz version:
Contact:

Node-RED Flow: Domoticz ↔ WLED Two-Way Sync with Dynamic Device Detection

Post by drogert »

Hi everyone,

I’ve created a Node-RED flow that synchronizes Domoticz switches with WLED instances in both directions, and I owe a huge thanks to Grok 3 from xAI for making it possible. Working with Grok 3 was an amazing experience—I learned a ton about Node-RED, JSON, and flow design while getting tons of help refining this from a basic idea into something flexible and powerful. It’s been a fantastic journey, and I’m excited to share the result with you all! This flow supports single-segment and multi-segment WLED devices, plus dynamic detection of new instances. Here’s everything you need to get it running:

Functionality
This flow:
Syncs Domoticz switches to WLED: Turn a switch on/off or adjust brightness/color in Domoticz, and WLED updates accordingly.
Syncs WLED to Domoticz: Toggle WLED via its UI or physical buttons, and Domoticz reflects the change.
Supports multi-segment WLED devices (e.g., separate control for Keukenkast segments 0 and 1).
Dynamically detects new WLED instances via MQTT button topics (wled/+/button/#) and prompts for IP configuration.
Preserves colors across syncs (e.g., red stays red, not reset to white).
Uses a 1-second debounce to prevent rapid sync loops.
Provides debug outputs for troubleshooting.

The Flow Code
Here’s the complete flow. Import it into Node-RED, then configure the "Setup WLED Devices" node as described below.

Code: Select all

[
    {
        "id": "setup-wled-devices",
        "type": "inject",
        "z": "fba59cdb0ac5ee46",
        "name": "Setup WLED Devices",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "wled/setup",
        "payload": "{}",
        "payloadType": "json",
        "x": 100,
        "y": 40,
        "wires": [["update-wled-devices"]]
    },
    {
        "id": "update-wled-devices",
        "type": "function",
        "z": "fba59cdb0ac5ee46",
        "name": "Update WLED Devices",
        "func": "if (msg.topic === 'wled/setup') {\n    flow.set('wledDevices', msg.payload.devices);\n    node.status({fill: 'green', shape: 'dot', text: 'Devices Updated'});\n} else {\n    const currentDevices = flow.get('wledDevices') || {};\n    const wledTopic = msg.topic.split('/')[1];\n    if (!currentDevices[`wled/${wledTopic}`]) {\n        currentDevices[`wled/${wledTopic}`] = {ip: null, segments: []};\n        flow.set('wledDevices', currentDevices);\n        node.status({fill: 'yellow', shape: 'ring', text: `Added ${wledTopic}, IP needed`});\n    }\n}\nreturn msg;",
        "outputs": 1,
        "x": 300,
        "y": 40,
        "wires": [[]]
    },
    {
        "id": "9dbd82c818781d70",
        "type": "mqtt in",
        "z": "fba59cdb0ac5ee46",
        "name": "Domoticz Status",
        "topic": "domoticz/out",
        "qos": "1",
        "broker": "2c68b3ed.13aabc",
        "inputs": 0,
        "x": 100,
        "y": 140,
        "wires": [["c31aa8734efac66e"]]
    },
    {
        "id": "c31aa8734efac66e",
        "type": "function",
        "z": "fba59cdb0ac5ee46",
        "name": "Pre-Filter",
        "func": "if (msg.payload === \"Connection Lost\") {\n    node.status({fill: \"red\", shape: \"ring\", text: \"Connection Lost\"});\n    return null;\n}\nif (typeof msg.payload !== 'string') {\n    return null;\n}\ntry {\n    msg.domoticzPayload = JSON.parse(msg.payload);\n    msg.source = 'domoticz';\n    node.status({fill: \"green\", shape: \"dot\", text: \"Parsed OK\"});\n} catch (e) {\n    node.status({fill: \"red\", shape: \"ring\", text: \"Invalid JSON\"});\n    return null;\n}\nreturn msg;",
        "outputs": 1,
        "x": 300,
        "y": 140,
        "wires": [["2ce7c59276bf697a"]]
    },
    {
        "id": "2ce7c59276bf697a",
        "type": "switch",
        "z": "fba59cdb0ac5ee46",
        "name": "Filter IDX",
        "property": "domoticzPayload.idx",
        "propertyType": "msg",
        "rules": [
            {"t": "eq", "v": "1525", "vt": "str"},
            {"t": "eq", "v": "1526", "vt": "str"},
            {"t": "eq", "v": "1528", "vt": "str"},
            {"t": "eq", "v": "1529", "vt": "str"},
            {"t": "eq", "v": "1530", "vt": "str"}
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 5,
        "x": 500,
        "y": 140,
        "wires": [
            ["38a72b3e4a8171e8"],
            ["38a72b3e4a8171e8"],
            ["38a72b3e4a8171e8"],
            ["38a72b3e4a8171e8"],
            ["38a72b3e4a8171e8"]
        ]
    },
    {
        "id": "38a72b3e4a8171e8",
        "type": "function",
        "z": "fba59cdb0ac5ee46",
        "name": "Filter Messages",
        "func": "if (typeof msg.domoticzPayload !== 'object' || !msg.domoticzPayload.idx || typeof msg.domoticzPayload.nvalue === 'undefined') {\n    node.status({fill: \"red\", shape: \"ring\", text: \"Invalid Message\"});\n    return null;\n}\nnode.status({fill: \"green\", shape: \"dot\", text: `IDX ${msg.domoticzPayload.idx} Received`});\nreturn msg;",
        "outputs": 1,
        "x": 700,
        "y": 140,
        "wires": [["8d01c56fa7e7b31b"]]
    },
    {
        "id": "8d01c56fa7e7b31b",
        "type": "function",
        "z": "fba59cdb0ac5ee46",
        "name": "Process Domoticz Command",
        "func": "if (msg.statusUpdate) {\n    node.status(msg.statusUpdate);\n    return null;\n}\nconst devices = flow.get('wledDevices') || {};\nconst idx = String(msg.domoticzPayload.idx);\nlet device = null;\nfor (const [key, dev] of Object.entries(devices)) {\n    const segMatch = dev.segments.find(seg => String(seg.idx) === idx);\n    if (segMatch) {\n        device = {ip: dev.ip, seg: segMatch.seg, wledTopic: key.split('/')[1]};\n        break;\n    }\n}\nif (!device) {\n    node.status({fill: 'red', shape: 'ring', text: `IDX ${idx}: Unknown`});\n    return [null, null];\n}\nconst nvalue = msg.domoticzPayload.nvalue;\nconst svalue1 = parseInt(msg.domoticzPayload.svalue1) || 0;\nconst color = msg.domoticzPayload.Color || null;\nconst desiredOn = (nvalue === 15 && svalue1 > 0) || (nvalue !== 0 && nvalue !== 15);\nconst cacheKey = device.seg !== null ? `seg${device.seg}` : 'state';\nconst segKey = device.seg !== null ? device.seg : 'main';\nconst cachedState = flow.get(`wled_${device.ip}_${cacheKey}`);\nconst cachedBrightness = flow.get(`wled_${device.ip}_brightness_${segKey}`) || 128;\nconst cachedRgb = flow.get(`wled_${device.ip}_rgb_${segKey}`) || {r: 255, g: 255, b: 255};\nconst lastUpdate = flow.get(`wled_${device.ip}_lastUpdate_${segKey}`) || 0;\n\nconst now = Date.now();\nif (msg.source === 'domoticz' && now - lastUpdate < 1000) {\n    node.status({fill: 'grey', shape: 'dot', text: `IDX ${idx}: Skipped (recent sync)`});\n    return [null, null];\n}\n\nconst brightness = desiredOn ? Math.max(Math.round((svalue1 / 100) * 255), 1) : 0;\nconst isBrightnessChange = desiredOn && brightness !== cachedBrightness;\nconst rgb = color && desiredOn ? {r: color.r || 0, g: color.g || 0, b: color.b || 0} : cachedRgb;\nconst isRgbChange = desiredOn && (rgb.r !== cachedRgb.r || rgb.g !== cachedRgb.g || rgb.b !== cachedRgb.b);\n\nif (cachedState === desiredOn && !isBrightnessChange && !isRgbChange) {\n    node.status({fill: 'green', shape: 'dot', text: `IDX ${idx}: Up-to-date`});\n    return [msg, null];\n}\n\nlet url, method, newPayload;\nif (device.seg !== null) {\n    method = 'POST';\n    url = `http://${device.ip}/json/state`;\n    newPayload = {seg: [{id: device.seg, on: desiredOn, col: [[rgb.r, rgb.g, rgb.b], [0], [0]], bri: brightness}]};\n} else {\n    method = 'GET';\n    url = `http://${device.ip}/win&T=${desiredOn ? 1 : 0}&A=${brightness}&R=${rgb.r}&G=${rgb.g}&B=${rgb.b}&W=0`;\n    newPayload = null;\n}\n\nmsg.method = method;\nmsg.url = url;\nmsg.payload = newPayload ? JSON.stringify(newPayload) : null;\nmsg.headers = newPayload ? {'Content-Type': 'application/json'} : {};\nmsg.idx = idx;\nmsg.deviceIp = device.ip;\nmsg.cacheKey = cacheKey;\nmsg.brightness = brightness;\nmsg.rgb = rgb;\n\nnode.status({fill: 'yellow', shape: 'ring', text: `IDX ${idx}: Sending`});\nreturn [null, msg];",
        "outputs": 2,
        "x": 900,
        "y": 140,
        "wires": [["030a27e22945c006"], ["dfa20f098e19406e"]]
    },
    {
        "id": "dfa20f098e19406e",
        "type": "http request",
        "z": "fba59cdb0ac5ee46",
        "name": "WLED Command",
        "method": "use",
        "ret": "txt",
        "url": "",
        "x": 1100,
        "y": 140,
        "wires": [["9632d9643c253131"]]
    },
    {
        "id": "9632d9643c253131",
        "type": "function",
        "z": "fba59cdb0ac5ee46",
        "name": "Update Cached State",
        "func": "const devices = flow.get('wledDevices') || {};\nconst idx = msg.idx;\nconst deviceIp = msg.deviceIp;\nconst cacheKey = msg.cacheKey;\nconst brightness = msg.brightness;\nconst rgb = msg.rgb;\nif (!idx || !deviceIp || !cacheKey || !msg.domoticzPayload) {\n    node.status({fill: 'red', shape: 'ring', text: 'Missing data'});\n    return null;\n}\nconst desiredOn = msg.domoticzPayload.nvalue !== 0;\nlet segKey = 'main';\nfor (const dev of Object.values(devices)) {\n    const segMatch = dev.segments.find(seg => String(seg.idx) === idx);\n    if (segMatch && dev.ip === deviceIp) {\n        segKey = segMatch.seg !== null ? segMatch.seg : 'main';\n        break;\n    }\n}\nflow.set(`wled_${deviceIp}_${cacheKey}`, desiredOn);\nflow.set(`wled_${deviceIp}_brightness_${segKey}`, brightness);\nflow.set(`wled_${deviceIp}_rgb_${segKey}`, rgb);\nflow.set(`wled_${deviceIp}_lastUpdate_${segKey}`, Date.now());\nnode.status({fill: 'blue', shape: 'dot', text: `IDX ${idx}: Updated`});\nreturn {payload: msg.payload, statusUpdate: {fill: 'green', shape: 'dot', text: `IDX ${idx}: Command Sent`}};",
        "outputs": 1,
        "x": 1300,
        "y": 140,
        "wires": [["442d0c880fcdc0a5"]]
    },
    {
        "id": "442d0c880fcdc0a5",
        "type": "link out",
        "z": "fba59cdb0ac5ee46",
        "name": "Link to Process",
        "mode": "link",
        "links": ["9ed0abe52f501048"],
        "x": 1500,
        "y": 140,
        "wires": []
    },
    {
        "id": "9ed0abe52f501048",
        "type": "link in",
        "z": "fba59cdb0ac5ee46",
        "name": "Link from Update",
        "links": ["442d0c880fcdc0a5"],
        "x": 850,
        "y": 220,
        "wires": [["8d01c56fa7e7b31b"]]
    },
    {
        "id": "030a27e22945c006",
        "type": "debug",
        "z": "fba59cdb0ac5ee46",
        "name": "Debug Domoticz Process",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "x": 1100,
        "y": 220,
        "wires": []
    },
    {
        "id": "c1591c2281433ada",
        "type": "mqtt in",
        "z": "fba59cdb0ac5ee46",
        "name": "WLED Status",
        "topic": "wled/+/v",
        "qos": "1",
        "broker": "2c68b3ed.13aabc",
        "inputs": 0,
        "x": 100,
        "y": 340,
        "wires": [["ef82b3cf080d4db1"]]
    },
    {
        "id": "ef82b3cf080d4db1",
        "type": "function",
        "z": "fba59cdb0ac5ee46",
        "name": "Parse WLED State",
        "func": "const devices = flow.get('wledDevices') || {};\nconst wledTopic = msg.topic.split('/')[1];\nconst deviceKey = `wled/${wledTopic}`;\nconst matchingDevice = devices[deviceKey];\nif (!matchingDevice) {\n    node.status({fill: 'red', shape: 'ring', text: `Unknown topic: ${wledTopic}`});\n    return [null, null];\n}\n\nconst device = matchingDevice;\n\nif (device.segments.length === 1 && device.segments[0].seg === null) {\n    const xml = msg.payload;\n    try {\n        const nrMatch = xml.match(/<nr>(\\d+)<\\/nr>/);\n        const acMatch = xml.match(/<ac>(\\d+)<\\/ac>/);\n        const clMatch = xml.match(/<cl>(\\d+)<\\/cl><cl>(\\d+)<\\/cl><cl>(\\d+)<\\/cl>/);\n        if (!nrMatch || !acMatch || !clMatch) {\n            node.status({fill: 'red', shape: 'ring', text: 'Invalid XML'});\n            return [null, null];\n        }\n        const [, nr] = nrMatch;\n        const [, brightness] = acMatch;\n        const [, r, g, b] = clMatch;\n\n        const isOn = nr === \"1\" && parseInt(brightness) > 0;\n        const brightnessPercent = Math.round((parseInt(brightness) / 255) * 100);\n\n        const idx = matchingDevice.segments[0].idx;\n        const cacheKey = 'state';\n        const segKey = 'main';\n        const lastUpdate = flow.get(`wled_${device.ip}_lastUpdate_${segKey}`) || 0;\n\n        const now = Date.now();\n        if (now - lastUpdate < 1000 || msg.buttonTrigger) {\n            node.status({fill: 'grey', shape: 'dot', text: `IDX ${idx}: Skipped (recent sync)`});\n            return [null, null];\n        }\n\n        let payload;\n        if (!isOn) {\n            payload = {command: 'switchlight', idx: parseInt(idx), switchcmd: 'Off'};\n        } else {\n            payload = {\n                command: 'setcolbrightnessvalue',\n                idx: parseInt(idx),\n                color: {m: 3, t: 0, r: parseInt(r), g: parseInt(g), b: parseInt(b), cw: 0, ww: 0},\n                brightness: brightnessPercent\n            };\n        }\n\n        msg.payload = JSON.stringify(payload);\n        msg.idx = idx;\n        msg.deviceIp = device.ip;\n        msg.cacheKey = cacheKey;\n        msg.brightness = parseInt(brightness);\n        msg.rgb = {r: parseInt(r), g: parseInt(g), b: parseInt(b)};\n        msg.source = 'wled';\n\n        node.status({fill: 'green', shape: 'dot', text: `WLED ${wledTopic} → IDX ${idx}`});\n        flow.set(`wled_${device.ip}_${cacheKey}`, isOn);\n        flow.set(`wled_${device.ip}_brightness_${segKey}`, parseInt(brightness));\n        flow.set(`wled_${device.ip}_rgb_${segKey}`, {r: parseInt(r), g: parseInt(g), b: parseInt(b)});\n        flow.set(`wled_${device.ip}_lastUpdate_${segKey}`, now);\n        return [msg, null];\n    } catch (e) {\n        node.status({fill: 'red', shape: 'ring', text: 'XML Parse Error'});\n        return [null, null];\n    }\n} else {\n    msg.url = `http://${device.ip}/json/state`;\n    msg.method = 'GET';\n    msg.idxList = device.segments.map(seg => seg.idx);\n    node.status({fill: 'yellow', shape: 'ring', text: `Fetching JSON for ${wledTopic}`});\n    return [null, msg];\n}",
        "outputs": 2,
        "x": 300,
        "y": 340,
        "wires": [["7e09e9790baf890d"], ["fetch-wled-json-state"]]
    },
    {
        "id": "fetch-wled-json-state",
        "type": "http request",
        "z": "fba59cdb0ac5ee46",
        "name": "Fetch WLED JSON State",
        "method": "use",
        "ret": "obj",
        "url": "",
        "x": 500,
        "y": 420,
        "wires": [["process-wled-json-state"]]
    },
    {
        "id": "process-wled-json-state",
        "type": "function",
        "z": "fba59cdb0ac5ee46",
        "name": "Process WLED JSON State",
        "func": "const devices = flow.get('wledDevices') || {};\nconst idxList = msg.idxList || [];\nif (!Array.isArray(idxList) || idxList.length === 0) return null;\n\nconst state = msg.payload;\nif (!state || !Array.isArray(state.seg)) {\n    node.status({fill: 'red', shape: 'ring', text: 'Invalid JSON state'});\n    return null;\n}\n\nconst messages = [];\nidxList.forEach(idx => {\n    let device = null;\n    let deviceSeg = null;\n    for (const [_, dev] of Object.entries(devices)) {\n        const segMatch = dev.segments.find(seg => String(seg.idx) === String(idx));\n        if (segMatch) {\n            device = dev;\n            deviceSeg = segMatch;\n            break;\n        }\n    }\n    if (!device || !deviceSeg) return;\n\n    const segState = deviceSeg.seg === null ? (state.seg && state.seg[0] ? state.seg[0] : state) : state.seg[deviceSeg.seg];\n    if (!segState) {\n        node.status({fill: 'red', shape: 'ring', text: `No segment data for IDX ${idx}`});\n        return;\n    }\n\n    const isOn = segState.on === true;\n    const brightness = segState.bri || state.bri || 0;\n    const brightnessPercent = Math.round((brightness / 255) * 100);\n    const col = segState.col && segState.col[0] ? segState.col[0] : (state.col && state.col[0] ? state.col[0] : [255, 255, 255]);\n    const [r, g, b] = col;\n\n    const cacheKey = deviceSeg.seg !== null ? `seg${deviceSeg.seg}` : 'state';\n    const segKey = deviceSeg.seg !== null ? deviceSeg.seg : 'main';\n    const lastUpdate = flow.get(`wled_${device.ip}_lastUpdate_${segKey}`) || 0;\n    const cachedState = flow.get(`wled_${device.ip}_${cacheKey}`);\n\n    const now = Date.now();\n    if (now - lastUpdate < 1000 || msg.buttonTrigger) {\n        node.status({fill: 'grey', shape: 'dot', text: `IDX ${idx}: Skipped (recent sync)`});\n        return;\n    }\n\n    if (cachedState !== isOn) {\n        let payload;\n        if (isOn) {\n            payload = {\n                command: 'setcolbrightnessvalue',\n                idx: parseInt(idx),\n                color: {m: 3, t: 0, r: r, g: g, b: b, cw: 0, ww: 0},\n                brightness: brightnessPercent\n            };\n        } else {\n            payload = {\n                command: 'switchlight',\n                idx: parseInt(idx),\n                switchcmd: 'Off'\n            };\n        }\n\n        messages.push({\n            payload: JSON.stringify(payload),\n            idx: idx,\n            deviceIp: device.ip,\n            cacheKey: cacheKey,\n            brightness: brightness,\n            rgb: {r: r, g: g, b: b},\n            source: 'wled',\n            topic: 'domoticz/in'\n        });\n        node.status({fill: 'green', shape: 'dot', text: `IDX ${idx}: Processed`});\n        flow.set(`wled_${device.ip}_${cacheKey}`, isOn);\n        flow.set(`wled_${device.ip}_lastUpdate_${segKey}`, now);\n    } else {\n        node.status({fill: 'grey', shape: 'dot', text: `IDX ${idx}: No change`});\n    }\n});\n\nreturn messages.length > 0 ? messages : null;",
        "outputs": 1,
        "x": 700,
        "y": 420,
        "wires": [["split-messages"]]
    },
    {
        "id": "split-messages",
        "type": "split",
        "z": "fba59cdb0ac5ee46",
        "name": "Split Messages",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "",
        "x": 900,
        "y": 420,
        "wires": [["7e09e9790baf890d", "debug-post-send"]]
    },
    {
        "id": "7e09e9790baf890d",
        "type": "mqtt out",
        "z": "fba59cdb0ac5ee46",
        "name": "Send to Domoticz",
        "topic": "domoticz/in",
        "qos": "1",
        "retain": "false",
        "broker": "2c68b3ed.13aabc",
        "x": 1100,
        "y": 340,
        "wires": []
    },
    {
        "id": "debug-wled-parse",
        "type": "debug",
        "z": "fba59cdb0ac5ee46",
        "name": "Debug WLED Parse",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "x": 1100,
        "y": 420,
        "wires": []
    },
    {
        "id": "debug-post-send",
        "type": "debug",
        "z": "fba59cdb0ac5ee46",
        "name": "Debug Post-Send",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "x": 1100,
        "y": 500,
        "wires": []
    },
    {
        "id": "button-mqtt-in",
        "type": "mqtt in",
        "z": "fba59cdb0ac5ee46",
        "name": "WLED Buttons",
        "topic": "wled/+/button/#",
        "qos": "0",
        "datatype": "auto-detect",
        "broker": "2c68b3ed.13aabc",
        "inputs": 0,
        "x": 100,
        "y": 640,
        "wires": [["button-process", "update-wled-devices"]]
    },
    {
        "id": "button-process",
        "type": "function",
        "z": "fba59cdb0ac5ee46",
        "name": "Process WLED Buttons",
        "func": "const devices = flow.get('wledDevices') || {};\nconst topicParts = msg.topic.split('/');\nconst wledTopic = topicParts[1];\nconst deviceKey = `wled/${wledTopic}`;\nconst idx = Object.entries(devices).flatMap(([_, dev]) => dev.segments.map(seg => ({idx: seg.idx, ip: dev.ip}))).find(d => devices[deviceKey]?.segments.some(seg => String(seg.idx) === String(d.idx)))?.idx;\nif (!idx || !devices[deviceKey]) {\n    node.status({fill: 'red', shape: 'ring', text: `Unknown topic: ${wledTopic}`});\n    return null;\n}\nconst device = devices[deviceKey];\n\nconst cacheKey = device.segments.length === 1 && device.segments[0].seg === null ? 'state' : `seg${device.segments.find(seg => String(seg.idx) === String(idx))?.seg}`;\nconst segKey = cacheKey === 'state' ? 'main' : parseInt(cacheKey.replace('seg', ''));\nconst lastUpdate = flow.get(`wled_${device.ip}_lastUpdate_${segKey}`) || 0;\n\nconst now = Date.now();\nif (now - lastUpdate < 1000) {\n    node.status({fill: 'grey', shape: 'dot', text: `IDX ${idx}: Skipped button (recent sync)`});\n    return null;\n}\n\nmsg.url = `http://${device.ip}/json/state`;\nmsg.method = 'GET';\nmsg.idx = idx;\nmsg.deviceIp = device.ip;\nmsg.cacheKey = cacheKey;\nmsg.source = 'button';\nmsg.buttonTrigger = true;\n\nnode.status({fill: 'yellow', shape: 'ring', text: `Fetching WLED state for IDX ${idx}`});\nreturn msg;",
        "outputs": 1,
        "x": 300,
        "y": 640,
        "wires": [["fetch-wled-state-button"]]
    },
    {
        "id": "fetch-wled-state-button",
        "type": "http request",
        "z": "fba59cdb0ac5ee46",
        "name": "Fetch WLED State (Button)",
        "method": "use",
        "ret": "obj",
        "url": "",
        "x": 500,
        "y": 640,
        "wires": [["sync-domoticz-button"]]
    },
    {
        "id": "sync-domoticz-button",
        "type": "function",
        "z": "fba59cdb0ac5ee46",
        "name": "Sync Domoticz Button",
        "func": "const devices = flow.get('wledDevices') || {};\nconst idx = msg.idx;\nlet device = null;\nlet deviceSeg = null;\nfor (const [_, dev] of Object.entries(devices)) {\n    const segMatch = dev.segments.find(seg => String(seg.idx) === String(idx));\n    if (segMatch) {\n        device = dev;\n        deviceSeg = segMatch;\n        break;\n    }\n}\nif (!device || !deviceSeg) {\n    node.status({fill: 'red', shape: 'ring', text: `IDX ${idx}: Unknown`});\n    return null;\n}\n\nconst state = msg.payload;\nlet isOn, brightness, brightnessPercent, r, g, b;\nif (deviceSeg.seg === null) {\n    const segState = state.seg && state.seg[0] ? state.seg[0] : state;\n    isOn = segState.on === true && (segState.bri || state.bri) > 0;\n    brightness = segState.bri || state.bri || 0;\n    brightnessPercent = Math.round((brightness / 255) * 100);\n    const col = segState.col && segState.col[0] ? segState.col[0] : (state.col && state.col[0] ? state.col[0] : [255, 255, 255]);\n    [r, g, b] = col;\n} else {\n    const segState = state.seg[deviceSeg.seg];\n    if (!segState) {\n        node.status({fill: 'red', shape: 'ring', text: `No segment data for IDX ${idx}`});\n        return null;\n    }\n    isOn = segState.on === true;\n    brightness = segState.bri || 0;\n    brightnessPercent = Math.round((brightness / 255) * 100);\n    const col = segState.col && segState.col[0] ? segState.col[0] : [255, 255, 255];\n    [r, g, b] = col;\n}\n\nconst cacheKey = deviceSeg.seg !== null ? `seg${deviceSeg.seg}` : 'state';\nconst segKey = deviceSeg.seg !== null ? deviceSeg.seg : 'main';\nconst lastUpdate = flow.get(`wled_${device.ip}_lastUpdate_${segKey}`) || 0;\nconst cachedState = flow.get(`wled_${device.ip}_${cacheKey}`);\n\nconst now = Date.now();\nif (now - lastUpdate < 1000) {\n    node.status({fill: 'grey', shape: 'dot', text: `IDX ${idx}: Skipped (recent sync)`});\n    return null;\n}\n\nlet payload;\nif (isOn) {\n    payload = {\n        command: 'setcolbrightnessvalue',\n        idx: parseInt(idx),\n        color: {m: 3, t: 0, r: r, g: g, b: b, cw: 0, ww: 0},\n        brightness: brightnessPercent\n    };\n} else {\n    payload = {\n        command: 'switchlight',\n        idx: parseInt(idx),\n        switchcmd: 'Off'\n    };\n}\n\nif (cachedState !== isOn) {\n    msg.payload = JSON.stringify(payload);\n    msg.idx = idx;\n    msg.deviceIp = device.ip;\n    msg.cacheKey = cacheKey;\n    msg.brightness = brightness;\n    msg.rgb = {r: r, g: g, b: b};\n    msg.source = 'wled';\n    msg.topic = 'domoticz/in';\n\n    node.status({fill: 'green', shape: 'dot', text: `Button sync IDX ${idx} to Domoticz`});\n    flow.set(`wled_${device.ip}_${cacheKey}`, isOn);\n    if (isOn) {\n        flow.set(`wled_${device.ip}_brightness_${segKey}`, brightness);\n        flow.set(`wled_${device.ip}_rgb_${segKey}`, {r: r, g: g, b: b});\n    }\n    flow.set(`wled_${device.ip}_lastUpdate_${segKey}`, now);\n\n    return msg;\n} else {\n    node.status({fill: 'grey', shape: 'dot', text: `IDX ${idx}: No change`});\n    return null;\n}",
        "outputs": 1,
        "x": 700,
        "y": 640,
        "wires": [["7e09e9790baf890d", "debug-button-sync"]]
    },
    {
        "id": "debug-button-sync",
        "type": "debug",
        "z": "fba59cdb0ac5ee46",
        "name": "Debug Button Sync",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "x": 900,
        "y": 720,
        "wires": []
    },
    {
        "id": "2c68b3ed.13aabc",
        "type": "mqtt-broker",
        "name": "",
        "broker": "localhost",
        "port": "1883",
        "clientid": "",
        "usetls": false,
        "compatmode": false,
        "keepalive": "60",
        "cleansession": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "closeTopic": "",
        "closePayload": "",
        "willTopic": "",
        "willQos": "0",
        "willPayload": ""
    }
]
Manual: How to Implement
Import the Flow:
Copy the code above.
In Node-RED, go to Menu > Import (or Ctrl+I), paste the code, and click Import.
Deploy the flow.
Configure WLED Devices:
Open the "Setup WLED Devices" node (top-left, inject type).
Click the ... button next to payload.
Replace the empty {} with your WLED configuration in this format:

Code: Select all

{
    "devices": {
        "wled/<your-device-name>": {
            "ip": "<device-ip>",
            "segments": [
                {"idx": <domoticz-idx>, "seg": <segment-id-or-null>}
            ]
        }
    }
}
Ensure Payload Type is set to JSON, save, and deploy.
Adjust Domoticz IDX Filter:
Open the "Filter IDX" node (a switch node).
Update the IDX values in the rules to match your Domoticz switches (default is 1525–1530).
Configure MQTT Broker:
Open the "MQTT Broker" node (bottom).
Set your MQTT broker’s address and port (default is localhost:1883).
Save and deploy.

Test:
Toggle a Domoticz switch and check if WLED updates.
Toggle a WLED device (UI or button) and verify Domoticz syncs.
Watch the debug sidebar ("Debug Domoticz Process," "Debug WLED Parse," etc.) for outputs.
Examples

Here are some payload examples for the "Setup WLED Devices" node:
Single-Segment Device (e.g., a PC LED strip):

Code: Select all

{
    "devices": {
        "wled/pcled": {
            "ip": "192.168.1.113",
            "segments": [
                {"idx": 1530, "seg": null}
            ]
        }
    }
}
Multi-Segment Device (e.g., Keukenkast with two segments):

Code: Select all

{
    "devices": {
        "wled/keukenkast": {
            "ip": "192.168.1.201",
            "segments": [
                {"idx": 1525, "seg": 0},
                {"idx": 1526, "seg": 1}
            ]
        }
    }
}
Multiple Devices:

Code: Select all

{
    "devices": {
        "wled/keukenkast": {
            "ip": "192.168.1.201",
            "segments": [
                {"idx": 1525, "seg": 0},
                {"idx": 1526, "seg": 1}
            ]
        },
        "wled/pcled": {
            "ip": "192.168.1.113",
            "segments": [
                {"idx": 1530, "seg": null}
            ]
        }
    }
}
Notes
Requirements: Node-RED with MQTT and HTTP request nodes installed, WLED configured with MQTT (publishing to wled/+/v and wled/+/button/#), and Domoticz MQTT enabled.

Dynamic Detection: If a new WLED device sends a button press, "Update WLED Devices" will add it with ip: null. Edit the inject node’s payload to include its IP and IDX.

IDX Customization: You must adjust the IDX values in the "Filter IDX" node to match your Domoticz setup. Open the node, add more IDX rules if you have additional switches, or delete unused ones as needed (default is 1525–1530).

Virtual MQTT RGB Buttons: For Domoticz and WLED to sync correctly, you need to create virtual MQTT RGB buttons in Domoticz. Add these via the Domoticz UI (Hardware > Dummy > Create Virtual Sensors), set them as "RGB" type, and note their IDX values to use in the flow’s configuration.

WLED MQTT Configuration: You must configure WLED to send and receive MQTT messages for this flow to work. Here’s a brief manual:
Access the WLED web interface (e.g., http://<wled-ip>).
Go to Settings > Sync Interfaces.
Enable MQTT and set:
Broker: Your MQTT broker address (e.g., localhost or your broker’s IP).
Port: Your MQTT port (default is 1883).
Username/Password: (if required by your broker).
Device Topic: Use wled/<device-name> (e.g., wled/keukenkast), matching the flow’s configuration.
Enable Publish on button press to send button events to MQTT.
Save and reboot WLED. Verify it publishes to wled/<device-name>/v and wled/<device-name>/button/#.

Troubleshooting: Check the debug nodes in the sidebar for status messages (green for success, red for errors).

Let me know if you have questions or suggestions for tweaking it further. Working with Grok 3 was an incredible learning experience, and I hope this flow helps others in the Domoticz community too!

Cheers,

DrogerT
RPI 3b+, PiFace 2, RFcom, Aeon Zwave USB, Aeon Home Energy Meter Gen 5, several Shelly switches, several ESP8266 sensors.
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest