Trouble with MQTT automation creating endless loops

Yes, I knew it wasn't perfect.
I had a bit of a Eureka moment at 04:00 this morning and realised, I think, the 'right' way to solve this problem. I just have to work out the best way to implement it. Will get back again in a few hours, hopefully.

I hope @Colin doesnt mind me chipping in. I ve been following this topic and had an idea of sorts that should solve it with a single function node, here is an example flow:

[{"id":"8c0695cc.00b06","type":"function","z":"75c76d0b.43994c","name":"propagate","func":"const dimmerList = [\"dimmer1\",\"dimmer2\",\"dimmer3\",\"dimmer4\"];\nlet receiving = context.get(\"receiving\") || false;\nlet source = context.get(\"source\");\nfunction propagate (value, items, noSend) {\n    items.forEach(item => {\n        if (item !== noSend) {\n            node.send({payload:value, topic:`cmnd/lighting/${item}/dimmer`});\n        }\n    });\n}\nif (!receiving) {\n    context.set(\"receiving\", true);\n    const splitTopic = msg.topic.split(\"/\");\n    context.set(\"source\", splitTopic[2]);\n    const propTimeout = setTimeout(()=>{\n        context.set(\"receiving\",false);\n    },1000);\n    context.set(\"propTimeout\", propTimeout);\n    propagate(msg.payload, dimmerList, splitTopic[2]);\n} else if (msg.topic === source) {\n    let propTimeout = context.get(\"propTimeout\");\n    clearTimeout(propTimeout);\n    propTimeout = setTimeout(()=>{\n        context.set(\"receiving\",false);\n    },1000);\n    context.set(\"propTimeout\", propTimeout);\n    propagate(msg.payload, dimmerList, source);\n}\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":470,"y":160,"wires":[["c858fedf.7a5be8","5cfd72b6.a61d54"]]},{"id":"14ab4743.cad291","type":"inject","z":"75c76d0b.43994c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"stat/lighting/dimmer3/RESULT","payload":"5","payloadType":"num","x":190,"y":180,"wires":[["8c0695cc.00b06"]]},{"id":"f38b1b0d.55cb7","type":"inject","z":"75c76d0b.43994c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"stat/lighting/dimmer2/RESULT","payload":"60","payloadType":"num","x":190,"y":140,"wires":[["8c0695cc.00b06"]]},{"id":"6168c634.deaf08","type":"inject","z":"75c76d0b.43994c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"stat/lighting/dimmer1/RESULT","payload":"40","payloadType":"num","x":190,"y":100,"wires":[["8c0695cc.00b06"]]},{"id":"fa370b92.e398f","type":"inject","z":"75c76d0b.43994c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"stat/lighting/dimmer4/RESULT","payload":"0","payloadType":"num","x":190,"y":220,"wires":[["8c0695cc.00b06"]]},{"id":"c858fedf.7a5be8","type":"debug","z":"75c76d0b.43994c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":690,"y":160,"wires":[]},{"id":"5cfd72b6.a61d54","type":"delay","z":"75c76d0b.43994c","name":"","pauseType":"delay","timeout":"250","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":510,"y":220,"wires":[["416297ac.416c28"]]},{"id":"416297ac.416c28","type":"function","z":"75c76d0b.43994c","name":"simulate mqtt result","func":"const item = msg.topic.split(\"/\")[2];\nmsg.topic = `stat/lighting/${item}/RESULT`;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":710,"y":220,"wires":[["8c0695cc.00b06"]]}]


This uses a single function node (propagate above) with the following content (the other nodes are all just there to simulate the mqtt cmnd and result loops):

const dimmerList = ["dimmer1","dimmer2","dimmer3","dimmer4"];
let receiving = context.get("receiving") || false;
let source = context.get("source");
function propagate (value, items, noSend) {
    items.forEach(item => {
        if (item !== noSend) {
            node.send({payload:value, topic:`cmnd/lighting/${item}/dimmer`});
        }
    });
}
if (!receiving) {
    context.set("receiving", true);
    const splitTopic = msg.topic.split("/");
    context.set("source", splitTopic[2]);
    const propTimeout = setTimeout(()=>{
        context.set("receiving",false);
    },500);
    context.set("propTimeout", propTimeout);
    propagate(msg.payload, dimmerList, splitTopic[2]);
} else if (msg.topic === source) {
    let propTimeout = context.get("propTimeout");
    clearTimeout(propTimeout);
    propTimeout = setTimeout(()=>{
        context.set("receiving",false);
    },500);
    context.set("propTimeout", propTimeout);
    propagate(msg.payload, dimmerList, source);
}
return null;

When a message from a dimmer arrives it sets this dimmer as the source with a timeout. Within that timeout it will propagate each message of the source to all other members of the list but not itself. The propagation timeout gets extended as long as new messages from the source arrive. In this period the input from the other items gets ignored.
So this keeps all dimmers in Sync in nearly real-time and unison without a loop appearing hopefully.
Johannes

1 Like

Right, this should be close. You will need to install node-red-contrib-multiple-queue. I have raised a feature request on that node that will simplify the flow a bit if it is implemented.

[{"id":"72d0d7ba.711f5","type":"m-queue","z":"f7c3ff8f.e6ce1","name":"","queueSelect":"topic","controlFlag":"control","defaultQueue":"default","allQueues":"all","triggerCmd":"trigger","statusCmd":"status","pauseCmd":"pause","resumeCmd":"resume","flushCmd":"flush","resetCmd":"reset","peekCmd":"peek","dropCmd":"drop","maximumCmd":"maximum","newestCmd":"newest","protectCmd":"protect","deleteCmd":"delete","paused":false,"protect":false,"keepNewestDefault":false,"maxSizeDefault":100,"protectDefault":true,"persist":false,"newValue":"value","storeName":"memoryOnly","statusOutput":false,"outputs":1,"x":440,"y":1440,"wires":[["bd6f5c1a.8374c","75295c4a.3bdae4"]]},{"id":"bc2ce950.6cd85","type":"function","z":"f7c3ff8f.e6ce1","name":"Insert Flush command","func":"// insert a queue flush command after the message so that it passes\n// straight through the m-queue gate if it is open\nnode.send(msg)\nnode.send ({topic: msg.topic, control: true, payload: \"flush\"})\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":180,"y":1440,"wires":[["72d0d7ba.711f5"]]},{"id":"bd6f5c1a.8374c","type":"trigger","z":"f7c3ff8f.e6ce1","name":"","op1":"","op2":"","op1type":"pay","op2type":"pay","duration":"250","extend":true,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":2,"x":360,"y":1520,"wires":[["b701bd2c.8bc32"],["cf239bcd.7ed818"]]},{"id":"b701bd2c.8bc32","type":"function","z":"f7c3ff8f.e6ce1","name":"Block other locations","func":"const locations = flow.get(\"locations\")\nfor (let i = 0; i < locations.length; i++) {\n    if (msg.topic.split(\"/\")[2] !== locations[i]) {\n        const topic = `stat/lighting/${locations[i]}/RESULT`\n        node.send({topic: topic, control: true, payload: \"pause\"})    \n    }\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":580,"y":1500,"wires":[["c057b6bc.04baa8"]]},{"id":"9304ad7e.f36548","type":"inject","z":"f7c3ff8f.e6ce1","name":"Startup","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":1640,"wires":[["b31e0c38.fb4af8"]]},{"id":"b31e0c38.fb4af8","type":"change","z":"f7c3ff8f.e6ce1","name":"Initialise flow.locations ","rules":[{"t":"set","p":"locations","pt":"flow","to":"[\"hallwaydimmer\",\"stairwell\"]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":1640,"wires":[["9e67d61e.f87478"]]},{"id":"cf239bcd.7ed818","type":"function","z":"f7c3ff8f.e6ce1","name":"Resume all","func":"//return {topic: \"all\", control: true, payload: \"resume\"}\nconst locations = flow.get(\"locations\")\nfor (let i = 0; i < locations.length; i++) {\n    if (msg.topic.split(\"/\")[2] !== locations[i]) {\n        const topic = `stat/lighting/${locations[i]}/RESULT`\n        node.send({topic: topic, control: true, payload: \"resume\"})    \n    }\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":570,"y":1540,"wires":[["c057b6bc.04baa8"]]},{"id":"75295c4a.3bdae4","type":"function","z":"f7c3ff8f.e6ce1","name":"Send to other locations","func":"const locations = flow.get(\"locations\")\nfor (let i = 0; i < locations.length; i++) {\n    if (msg.topic.split(\"/\")[2] !== locations[i]) {\n        const topic = `cmnd/lighting/${locations[i]}/dimmer`\n        node.send({payload: msg.payload, topic: topic})    \n    }\n\n}\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":680,"y":1440,"wires":[["32591db2.671352"]]},{"id":"323a82f9.254f7e","type":"mqtt in","z":"f7c3ff8f.e6ce1","name":"","topic":"stat/lighting/hallwaydimmer/RESULT","qos":"0","datatype":"json","broker":"577d2db3.1e1f7c","x":160,"y":1320,"wires":[["2a8c0f65.d7198"]]},{"id":"d796b8ac.4248d","type":"mqtt in","z":"f7c3ff8f.e6ce1","name":"","topic":"stat/lighting/stairwell/RESULT","qos":"0","datatype":"json","broker":"577d2db3.1e1f7c","x":180,"y":1360,"wires":[["2a8c0f65.d7198"]]},{"id":"2a8c0f65.d7198","type":"switch","z":"f7c3ff8f.e6ce1","name":"Dimmer?","property":"payload","propertyType":"msg","rules":[{"t":"hask","v":"Dimmer","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":440,"y":1340,"wires":[["ee57edd8.3d1cd"]]},{"id":"ee57edd8.3d1cd","type":"change","z":"f7c3ff8f.e6ce1","name":"Move to payload","rules":[{"t":"move","p":"payload.Dimmer","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":600,"y":1340,"wires":[["a3a5eec7.9daea","bc2ce950.6cd85"]]},{"id":"9e67d61e.f87478","type":"function","z":"f7c3ff8f.e6ce1","name":"Create queues","func":"const locations = flow.get(\"locations\")\nfor (let i = 0; i < locations.length; i++) {\n    const topic = `stat/lighting/${locations[i]}/RESULT`\n    // create the queue\n    node.send({topic: topic, payload: \"\"})\n    // empty it\n    node.send({topic:topic, control: true, payload: \"reset\"})\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":540,"y":1640,"wires":[["c057b6bc.04baa8"]]},{"id":"32591db2.671352","type":"mqtt out","z":"f7c3ff8f.e6ce1","name":"","topic":"","qos":"0","retain":"","broker":"577d2db3.1e1f7c","x":850,"y":1440,"wires":[]},{"id":"c057b6bc.04baa8","type":"link out","z":"f7c3ff8f.e6ce1","name":"","links":["dbe9cae2.0a0648"],"x":735,"y":1540,"wires":[]},{"id":"dbe9cae2.0a0648","type":"link in","z":"f7c3ff8f.e6ce1","name":"","links":["c057b6bc.04baa8"],"x":155,"y":1480,"wires":[["72d0d7ba.711f5"]]},{"id":"577d2db3.1e1f7c","type":"mqtt-broker","name":"MQTT","broker":"192.168.4.36","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","willTopic":"","willQos":"0","willRetain":"false","willPayload":""}]

I think that implements in js pretty much what my flow does in nodes.

Did you try it using a couple of sliders to make sure it works under duress?

Here is a working flow using a couple of dashboard sliders.

[{"id":"6afd42d5.9e3de4","type":"ui_slider","z":"f7c3ff8f.e6ce1","name":"","label":"hallwaydimmer","tooltip":"","group":"903a6ab8.4f4ca8","order":1,"width":0,"height":0,"passthru":true,"outs":"all","topic":"stat/lighting/hallwaydimmer/RESULT","min":0,"max":"100","step":1,"x":140,"y":1080,"wires":[["bc2ce950.6cd85"]]},{"id":"47c82964.fbd088","type":"ui_slider","z":"f7c3ff8f.e6ce1","name":"","label":"stairwell","tooltip":"","group":"903a6ab8.4f4ca8","order":1,"width":0,"height":0,"passthru":true,"outs":"all","topic":"stat/lighting/stairwell/RESULT","min":0,"max":"100","step":1,"x":160,"y":1120,"wires":[["bc2ce950.6cd85"]]},{"id":"12e25b9a.3ccee4","type":"switch","z":"f7c3ff8f.e6ce1","name":"","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"hallway","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":350,"y":1220,"wires":[["741dc401.2f8474"],["e542e639.b5e068"]]},{"id":"767ee89b.d202a","type":"delay","z":"f7c3ff8f.e6ce1","name":"","pauseType":"delay","timeout":"0.05","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":190,"y":1220,"wires":[["12e25b9a.3ccee4"]]},{"id":"741dc401.2f8474","type":"change","z":"f7c3ff8f.e6ce1","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"stat/lighting/hallwaydimmer/RESULT","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":1200,"wires":[["6afd42d5.9e3de4"]]},{"id":"e542e639.b5e068","type":"change","z":"f7c3ff8f.e6ce1","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"stat/lighting/stairwell/RESULT","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":1260,"wires":[["47c82964.fbd088"]]},{"id":"72d0d7ba.711f5","type":"m-queue","z":"f7c3ff8f.e6ce1","name":"","queueSelect":"topic","controlFlag":"control","defaultQueue":"default","allQueues":"all","triggerCmd":"trigger","statusCmd":"status","pauseCmd":"pause","resumeCmd":"resume","flushCmd":"flush","resetCmd":"reset","peekCmd":"peek","dropCmd":"drop","maximumCmd":"maximum","newestCmd":"newest","protectCmd":"protect","deleteCmd":"delete","paused":false,"protect":false,"keepNewestDefault":false,"maxSizeDefault":100,"protectDefault":true,"persist":false,"newValue":"value","storeName":"memoryOnly","statusOutput":false,"outputs":1,"x":440,"y":1440,"wires":[["bd6f5c1a.8374c","75295c4a.3bdae4"]]},{"id":"bc2ce950.6cd85","type":"function","z":"f7c3ff8f.e6ce1","name":"Insert Flush command","func":"// insert a queue flush command after the message so that it passes\n// straight through the m-queue gate if it is open\nnode.send(msg)\nnode.send ({topic: msg.topic, control: true, payload: \"flush\"})\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":160,"y":1440,"wires":[["72d0d7ba.711f5"]]},{"id":"bd6f5c1a.8374c","type":"trigger","z":"f7c3ff8f.e6ce1","name":"","op1":"","op2":"","op1type":"pay","op2type":"pay","duration":"250","extend":true,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":2,"x":360,"y":1520,"wires":[["b701bd2c.8bc32"],["cf239bcd.7ed818"]]},{"id":"b701bd2c.8bc32","type":"function","z":"f7c3ff8f.e6ce1","name":"Block other locations","func":"const locations = flow.get(\"locations\")\nfor (let i = 0; i < locations.length; i++) {\n    if (msg.topic.split(\"/\")[2] !== locations[i]) {\n        const topic = `stat/lighting/${locations[i]}/RESULT`\n        node.send({topic: topic, control: true, payload: \"pause\"})    \n    }\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":580,"y":1500,"wires":[["c057b6bc.04baa8"]]},{"id":"9304ad7e.f36548","type":"inject","z":"f7c3ff8f.e6ce1","name":"Startup","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":1640,"wires":[["b31e0c38.fb4af8"]]},{"id":"b31e0c38.fb4af8","type":"change","z":"f7c3ff8f.e6ce1","name":"Initialise flow.locations ","rules":[{"t":"set","p":"locations","pt":"flow","to":"[\"hallwaydimmer\",\"stairwell\"]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":1640,"wires":[["9e67d61e.f87478"]]},{"id":"cf239bcd.7ed818","type":"function","z":"f7c3ff8f.e6ce1","name":"Resume all","func":"//return {topic: \"all\", control: true, payload: \"resume\"}\nconst locations = flow.get(\"locations\")\nfor (let i = 0; i < locations.length; i++) {\n    if (msg.topic.split(\"/\")[2] !== locations[i]) {\n        const topic = `stat/lighting/${locations[i]}/RESULT`\n        node.send({topic: topic, control: true, payload: \"resume\"})    \n    }\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":570,"y":1540,"wires":[["c057b6bc.04baa8"]]},{"id":"75295c4a.3bdae4","type":"function","z":"f7c3ff8f.e6ce1","name":"Send to other locations","func":"const locations = flow.get(\"locations\")\nfor (let i = 0; i < locations.length; i++) {\n    if (msg.topic.split(\"/\")[2] !== locations[i]) {\n        const topic = `cmnd/lighting/${locations[i]}/dimmer`\n        node.send({payload: msg.payload, topic: topic})    \n    }\n\n}\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":680,"y":1440,"wires":[["32591db2.671352","767ee89b.d202a"]]},{"id":"9e67d61e.f87478","type":"function","z":"f7c3ff8f.e6ce1","name":"Create queues","func":"const locations = flow.get(\"locations\")\nfor (let i = 0; i < locations.length; i++) {\n    const topic = `stat/lighting/${locations[i]}/RESULT`\n    // create the queue\n    node.send({topic: topic, payload: \"\"})\n    // empty it\n    node.send({topic:topic, control: true, payload: \"reset\"})\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":540,"y":1640,"wires":[["c057b6bc.04baa8"]]},{"id":"c057b6bc.04baa8","type":"link out","z":"f7c3ff8f.e6ce1","name":"","links":["dbe9cae2.0a0648"],"x":735,"y":1540,"wires":[]},{"id":"dbe9cae2.0a0648","type":"link in","z":"f7c3ff8f.e6ce1","name":"","links":["c057b6bc.04baa8"],"x":155,"y":1480,"wires":[["72d0d7ba.711f5"]]},{"id":"903a6ab8.4f4ca8","type":"ui_group","name":"Home","tab":"e2e6f4f5.56f91","order":1,"disp":false,"width":"6","collapse":false},{"id":"e2e6f4f5.56f91","type":"ui_tab","name":"Homet","icon":"dashboard","order":3,"disabled":false,"hidden":false}]


image
Here it is.
Edit:
I also just tried it with two actual Shelly dimmers that I have connected to my bedside lamps and they stay in sink without loops no matter if I use them from my dashboard or use the physical switch on one of them. All I did for that was:

2 Likes

Nice. As is often the case there is a choice between moderately complex js versus a few nodes.

1 Like

@JGKK

Hi, would you mind sharing your latest flow? I have tried to build it up from your screenshots, it works kind of "com-si-com-sa". I think it is not really reacting properly on quick slider movements. But it might be in my re-construction there is something wrong

If you change different sliders very quickly after each other you will have to change the propagation timeout to be lower than i posted it in the first function.
By default I set it to 1000ms or 1 second so if you change one slider and than within 1 second another the second one will not get picked up by the others as it’s within the propagation timeout.
Change it to be something like 200 ms in both timeouts in the function node.
Ideally this timeout should in real life be longer than the time it takes for the mqtt roundtrip: mqtt-command -> device changes its physical state -> device gives state feedback back over mqtt.
I think this is more an issue with using virtual sliders as you can move your finger much more quickly than you could walk from light switch to light switch.
In real life you would only have one dashboard slider as the 4 dimmers all control the same thing.
That’s the only thing I changed in the function and the sliders are set to send on release.

this is with 200ms, the function node is connecting two real life shelly dimmer 2s in my bedroom and those sliders are directly controlling them:
image
This was as fast as I could move my fingers.

Dear Johannes, understood, that was it, now it works as expected I guess. But this solution will then not send/show intermediate values that was originally asked for by @reverendalc. I tried now with "continuosly while sliding" and only 10ms. It seems to work fine so far even if I guess it generates many more messages

Testing here with 5ms, slider values 100-0, step size 1
Works fine!!!

https://drive.google.com/file/d/1Ygi48CJGnYb4mHqLzdUs0xcEmVECTGLk/view?usp=sharing

It will also work with intermediates no problem.
I think the problem you had was that dashboard sliders set to continuously output are not a very good representation of a physical dimmer as you will get race conditions on the slider you are actually changing if set to also accept an input due to the high output rate in that mode combined with if you directly loop it back. If you want intermediate feedback from a dashboard slider I would do something like this:

[{"id":"a9b6c09c.9da84","type":"function","z":"88d30de4.d0a868","name":"propagate","func":"const dimmerList = [\"slider1\",\"slider2\",\"slider3\"];\nlet receiving = context.get(\"receiving\") || false;\nlet source = context.get(\"source\");\nfunction propagate (value, items, noSend) {\n    items.forEach(item => {\n        if (item !== noSend) {\n            node.send({payload:value, topic:`items/${item}/command`});\n        }\n    });\n}\nif (!receiving) {\n    context.set(\"receiving\", true);\n    const splitTopic = msg.topic.split(\"/\");\n    context.set(\"source\", splitTopic[1]);\n    const propTimeout = setTimeout(()=>{\n        context.set(\"receiving\",false);\n    },500);\n    context.set(\"propTimeout\", propTimeout);\n    propagate(msg.payload, dimmerList, splitTopic[1]);\n} else if (msg.topic === source) {\n    let propTimeout = context.get(\"propTimeout\");\n    clearTimeout(propTimeout);\n    propTimeout = setTimeout(()=>{\n        context.set(\"receiving\",false);\n    },500);\n    context.set(\"propTimeout\", propTimeout);\n    propagate(msg.payload, dimmerList, source);\n}\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":710,"y":160,"wires":[["c19589a8.76571"]]},{"id":"9998b53f.9268e","type":"ui_slider","z":"88d30de4.d0a868","name":"","label":"slider1","tooltip":"","group":"d7c34dd1.1e1a78","order":0,"width":0,"height":0,"passthru":true,"outs":"all","topic":"items/slider1/command","min":0,"max":"100","step":1,"x":210,"y":80,"wires":[["6a00018a.1e5e5","76b9ec45.f6c694"]]},{"id":"7c0a4f38.d9b82","type":"ui_slider","z":"88d30de4.d0a868","name":"","label":"slider2","tooltip":"","group":"d7c34dd1.1e1a78","order":0,"width":0,"height":0,"passthru":true,"outs":"all","topic":"items/slider2/command","min":0,"max":"100","step":1,"x":210,"y":160,"wires":[["3b86c6a5.f2e092","5116f483.2a7314"]]},{"id":"c1ba10e8.9b54d8","type":"ui_slider","z":"88d30de4.d0a868","name":"","label":"slider3","tooltip":"","group":"d7c34dd1.1e1a78","order":0,"width":0,"height":0,"passthru":true,"outs":"all","topic":"items/slider3/command","min":0,"max":"100","step":1,"x":210,"y":240,"wires":[["19bfa2e4.5bda9d","4fb927a5.83f6f"]]},{"id":"c19589a8.76571","type":"function","z":"88d30de4.d0a868","name":"command2state","func":"const newTopic = msg.topic.split(\"/\")[1];\nmsg.topic = `items/${newTopic}/state`;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":720,"y":320,"wires":[["ef2b6c5a.030d7"]]},{"id":"6a00018a.1e5e5","type":"delay","z":"88d30de4.d0a868","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"0.1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":410,"y":60,"wires":[["f1d9d39f.e66db8"]]},{"id":"76b9ec45.f6c694","type":"trigger","z":"88d30de4.d0a868","name":"","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"200","extend":true,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":420,"y":100,"wires":[["f1d9d39f.e66db8"]]},{"id":"3b86c6a5.f2e092","type":"delay","z":"88d30de4.d0a868","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"0.1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":410,"y":140,"wires":[["f1d9d39f.e66db8"]]},{"id":"19bfa2e4.5bda9d","type":"delay","z":"88d30de4.d0a868","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"0.1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"x":410,"y":220,"wires":[["f1d9d39f.e66db8"]]},{"id":"5116f483.2a7314","type":"trigger","z":"88d30de4.d0a868","name":"","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"200","extend":true,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":420,"y":180,"wires":[["f1d9d39f.e66db8"]]},{"id":"4fb927a5.83f6f","type":"trigger","z":"88d30de4.d0a868","name":"","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"200","extend":true,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":420,"y":260,"wires":[["f1d9d39f.e66db8"]]},{"id":"f1d9d39f.e66db8","type":"change","z":"88d30de4.d0a868","name":"connector","rules":[],"action":"","property":"","from":"","to":"","reg":false,"x":575,"y":160,"wires":[["a9b6c09c.9da84","c19589a8.76571"]],"l":false},{"id":"ef2b6c5a.030d7","type":"change","z":"88d30de4.d0a868","name":"connector","rules":[],"action":"","property":"","from":"","to":"","reg":false,"x":75,"y":320,"wires":[["c1ba10e8.9b54d8","7c0a4f38.d9b82","9998b53f.9268e"]],"l":false},{"id":"d7c34dd1.1e1a78","type":"ui_group","name":"Group 1","tab":"61904e51.d85b3","order":1,"disp":true,"width":"6","collapse":false},{"id":"61904e51.d85b3","type":"ui_tab","name":"Test","icon":"dashboard","disabled":false,"hidden":false}]


image
For most physical dimmers the rate limit and trigger to send last wouldn’t be necessary as all the physical dimmer devices I ever handled never send out state updates at such a rapid rate such as the dashboard slider node in continuous mode.
In the example above the propagation timeout is 500ms.
But as I said before in a real life application this timeout in the function node has to be longer than the roundtrip from command to physical device to state update over mqtt. This timeout has no actual influence on the immediacy of the change propagation to the other dimmers but only influenced how long the new messages from the source are not send on themselves.

wow, so many experienced minds on the matter, thank you all

I have implemented this flow:

all in all it works well. for some reason, occasionally the receiving dimmer gets set to lowest setting, no matter the result state of the other. I may dim 90%, 60%, or any arbitrary percentage, but about 1/6 manipulations results in.

I have to spend a little time dissecting this flow, it's funny there are so many different ways to accomplish this, each with their own nuance, benefit, and disadvantage

EDIT: I will put the switches into the wall today. this will be the true test, as a slave switch will need to be able to adjust the master switch in real time (with acceptable latency) for local dimming purposes.

At which rate do your switches send status updates when you locally dim on the device?

if I simply tap the dimmer panel, I receive a shotgun blast as follows:

1/16/2021, 9:57:30 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 60 }
1/16/2021, 9:57:30 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 60 }
1/16/2021, 9:57:30 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON" }
1/16/2021, 9:57:30 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 61 }

if I perform a swiping dim command, I get all of this, within 1sec it seems:

1/16/2021, 9:58:32 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 72 }
1/16/2021, 9:58:32 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 73 }
1/16/2021, 9:58:32 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON" }
1/16/2021, 9:58:32 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 72 }
1/16/2021, 9:58:32 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 68 }
1/16/2021, 9:58:32 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 60 }
1/16/2021, 9:58:32 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 47 }
1/16/2021, 9:58:32 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 35 }

the messages containing only POWER:ON are not relevant to this flow, as I have two separate flows to control these switches. the synchronous power states are handled by another flow which integrates with other proximate switches controlling separate fixtures

here is the debug from a dimming command starting at lowest setting and swiping to highest setting:

1/16/2021, 10:01:21 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 1 }
1/16/2021, 10:01:21 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 1 }
1/16/2021, 10:01:21 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 2 }
1/16/2021, 10:01:21 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 5 }
1/16/2021, 10:01:21 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON" }
1/16/2021, 10:01:21 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 10 }
1/16/2021, 10:01:22 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 23 }
1/16/2021, 10:01:22 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 40 }
1/16/2021, 10:01:22 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 56 }
1/16/2021, 10:01:22 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 76 }
1/16/2021, 10:01:22 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 88 }

with the following debug in place:
image

I'm reading exactly 20 messages/sec from the MQTT node

Check after the Dimmer? switch that it is correctly passing only the valid messages, then check after the next node that there are no spurious values.

ok here is debug:
image

and here is output from a single tap:

1/16/2021, 10:25:05 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 31 }
1/16/2021, 10:25:05 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 30 }
1/16/2021, 10:25:05 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 31 }
1/16/2021, 10:25:05 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 31 }
1/16/2021, 10:25:05 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 30 }
1/16/2021, 10:25:05 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 30 }
1/16/2021, 10:25:05 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 30 }

here is the output after a long slide:

1/16/2021, 10:27:09 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 31 }
1/16/2021, 10:27:09 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 29 }
1/16/2021, 10:27:09 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 29 }
1/16/2021, 10:27:09 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 31 }
1/16/2021, 10:27:09 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 31 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 29 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 29 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 29 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 34 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 43 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 54 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 34 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 34 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 66 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 43 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 43 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 80 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 54 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 54 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/hallwaydimmer/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 88 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 66 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 66 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 80 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 80 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 88 }
1/16/2021, 10:27:10 AMnode: f2aa39cc.2a512
stat/lighting/stairwell/RESULT : msg.payload : Object
{ POWER: "ON", Dimmer: 88 }

That looks ok, you will need to try and see where the zeros are coming from that make it go down the bottom occasionally. Maybe put debug on what is going to mqtt and see if you can catch it. If you send the debug to the console then you can scroll back in the log to see what happened.

If you want to give it a try 20 messages/second should work no problem with my propagation function node without any throttling. Just connect the mqtt state inputs of all dimmers to a function node with the propagation function:

and connect an mqtt out node with the topic left blank to the output. Of course you have to change the dummy names in the list defined in the first variable to your actual dimmer names.
Only thing I dont know is if the output messages are formatted right so maybe you need to add a change node for that or adapt it in the function node.
Id be interested to know if it works in your setup out of pure curiosity, even if you end up using the flow you developed with @Colin as that one looks very well thought out :grinning: