How to Only respond to 2nd message (within time window)

It looks like 'node-red-contrib-join-wait' could do this but I do not need to join messages from different paths; and I want to be sure there is no simpler way.

I have a motion sensor and I do not want a light to trigger on the first motion-message; only when it receives a second motion message within a couple of minutes.
I want it a little less 'triggerhappy'.

I was trying out some combos with delay and the powerful 'trigger' nodes, but it does not quite do what I want. I need this:

  • if a message arrives, delay it and start a timer;
  • if a second message arrives within the timer window, forward it immediately;
  • if the timer expires, the node resets
  • if a reset arrives, the node reset.

Try something like this, to reset set flow.time to 0

[{"id":"dc4124a4.3d357","type":"inject","z":"8d22ae29.7df6d","name":"","props":[{"p":"payload"},{"p":"delay","v":"3000","vt":"num"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":140,"y":2180,"wires":[["6577879e.0f24"]]},{"id":"6577879e.0f24","type":"switch","z":"8d22ae29.7df6d","name":"","property":"time","propertyType":"flow","rules":[{"t":"jsonata_exp","v":"$millis() - $flowContext(\"time\") < delay","vt":"jsonata"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":270,"y":2180,"wires":[["c62865e.8bb9798"],["9a405fcf.440928"]]},{"id":"9a405fcf.440928","type":"change","z":"8d22ae29.7df6d","name":"","rules":[{"t":"set","p":"time","pt":"flow","to":"$millis()","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":2220,"wires":[[]]},{"id":"c62865e.8bb9798","type":"change","z":"8d22ae29.7df6d","name":"","rules":[{"t":"set","p":"time","pt":"flow","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":2140,"wires":[["dcd8ea98.f4e16"]]},{"id":"dcd8ea98.f4e16","type":"debug","z":"8d22ae29.7df6d","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":570,"y":2080,"wires":[]}]
2 Likes

Wow! that's a good trick!
I ended up coding it, after all. Javascript can do exactly what I want.

This outputs the 1st message to the 1st output and the second and all following messages to a second output.
Wait for 10 seconds and it resets - and will block the first message again.
It also resets on a 'reset' message.
It's a kind of debounce but instead of dropping the 2nd message, it drops the 1st.
It called 'I insist' because you need to trigger it twice to get through.

[{"id":"5077248c.5ead5c","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"11cda614.9dc72a","type":"inject","z":"5077248c.5ead5c","name":"","props":[{"p":"payload.occupancy","v":"false","vt":"bool"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":120,"y":200,"wires":[["aea06469.6ec938"]]},{"id":"251c14fa.54998c","type":"debug","z":"5077248c.5ead5c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":640,"y":220,"wires":[]},{"id":"8a0485e2.5fa118","type":"inject","z":"5077248c.5ead5c","name":"true","props":[{"p":"payload.occupancy","v":"true","vt":"bool"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":110,"y":280,"wires":[["aea06469.6ec938"]]},{"id":"e11b04c4.e8dd08","type":"inject","z":"5077248c.5ead5c","name":"reset","props":[{"p":"reset","v":"true","vt":"bool"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":130,"y":140,"wires":[["aea06469.6ec938"]]},{"id":"aea06469.6ec938","type":"function","z":"5077248c.5ead5c","name":"I Insist","func":"// input:\n// messages. \n// Send only 2nd and following message within a given timeframe.\n// reset after 'msg reset'\n// reset if time window expires\n\n// implementation: node keeps track of a state. It can be in either one of \n// two modes: holding or Passthrough.\n// Holding mode: when it receives a message it does not send, but goes to Passthrough mode.\n// Passthrough mode:\n// Every message received will be sent on.\n// After N minutes after the last message, the node will leave Passthrough mode and fall back to Holding mode.\n// every message resets the timer, so it takes N mins without messages to fall back to Holding mode\n\nconst BLOCK=1;\nconst PASSTHROUGH=2;\n\nvar now = Date.now();\nvar startTimer=context.get('starttimer')||now;\nvar mode=context.get('mode')||BLOCK; \n\nnewMsg=[null,null];\n// case: Message has arrived.\n// handle reset message: Go to mode BLOCK\nif (msg.hasOwnProperty('reset') && msg.reset!=false)\n{\n    mode=BLOCK;\n    node.status({fill:\"blue\",shape:\"ring\",text:\"reset. now blocking.\"});\n    newMsg=[null,null];\n}\nelse // regular message, either block or forward it. \n{\n    // check timer for expiration, since we do not have a callback when it expires. First time we check is on this message-event.\n    if (mode==PASSTHROUGH && (now-startTimer) > 10*1000)\n    {\n        // timer expired. kill message and go to mode BLOCK\n        mode=BLOCK;\n    }\n    \n    if (mode==BLOCK)\n    {\n        // discard message. change mode. set timer.\n        mode=PASSTHROUGH;\n        startTimer=now;\n        node.status({fill:\"yellow\",shape:\"ring\",text:\"blocked 1 msg\"});\n        newMsg=[msg,null];\n    }\n    else\n    {\n        // mode is passthrough. \n        // reset timer\n        startTimer=now;\n        node.status({fill:\"green\",shape:\"ring\",text:\"forwarded 1 or more msg\"});\n        newMsg=[null,msg];\n    }\n} \n\ncontext.set('mode', mode);\ncontext.set('starttimer', startTimer);\nreturn(newMsg);","outputs":2,"noerr":0,"initialize":"","finalize":"","x":370,"y":220,"wires":[["251c14fa.54998c"],["251c14fa.54998c"]],"outputLabels":["First message ","Next messages"]}]```
1 Like

Do you need the time to be variable?

If not, what would be wrong with this try:

[{"id":"d8da1b21.0a152","type":"inject","z":"703c61ac.837e1","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1900,"y":670,"wires":[["45dd677b.59b1a8"]]},{"id":"45dd677b.59b1a8","type":"trigger","z":"703c61ac.837e1","name":"","op1":"","op2":"","op1type":"nul","op2type":"payl","duration":"250","extend":false,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":2100,"y":670,"wires":[["867ba56d.f744a"]]},{"id":"867ba56d.f744a","type":"debug","z":"703c61ac.837e1","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2310,"y":670,"wires":[]}]

Thanks, something like this has been on my TODO list for too long a time. This gets me most of the way there.

For my AI person detection security system the first detection image is often not the best because the person's head can be cut off when first walking into the camera field of view, since there will be more images a fraction of a second later, the later images are generally "better" to push as MMS text or Email notifications.

This gets me most of the way there, if only one message comes in during the interval I'll send it after the delay, but with multiple messages in the interval, the second or third is generally better.

for something like video though won't the standard trigger node work ? set it to send latest msg.payload as the second message - so the first detection triggers it (don't send anything)- then x frames/seconds later it sends the latest it has (rather than the original)?

I haven't fully investigated the trigger node, but it seems to lack the behavior of sending the original message (image) if a second doesn't come within the interval.

if set to send latest - then yes of course if nothing else arrives it will send the original as that was still the latest to arrive...

The node-red-contrib-queue-gate is doing (I think) what you are looking for.
From https://github.com/drmibell/node-red-contrib-queue-gate:

Save Most Recent Message (since version 1.1.0)

This flow, as noted above, saves the most recent message in the queue and releases it when a trigger , flush , or open command is received. (Note that if the open command is used, the gate will remain open until a queue or default command is received to restore the original mode of operation.) The q-gate is configured with Default State = queueing , Maximum Queue Length = 1 , and Keep Newest Messages = true .

So, if further messages are received, they can replace the old ones and if nothing arrives within the time frame you can flush the queue and you will get what is the most recent message available.

Thanks, lots of new ideas to try. But I'm more comfortable inside function nodes at the moment.

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.

@E1cid

I'm using your clever flow to monitor several window and door contact sensors. If they switch to a new state for longer than 30 seconds, then Alexa announces that the specific door or window sensor is open or closed. I was using a couple of trigger nodes and reset nodes (see below) and that worked great, except it would ultimately always make an announcement of some sort.

[{"id":"c3ff746c4dd5d8a3","type":"group","z":"b5a44a6b8fa524cc","style":{"stroke":"#2b2b2b","stroke-opacity":"1","fill":"#181818","fill-opacity":"0.5","label":true,"label-position":"nw","color":"#cccccc"},"nodes":["3a2d7a1c82fdcead","6bc937df4c81a157","fe8e5cd980ce8d89","4b50fa847e306f12","36ca26d642338046","9930e99214c0ce15","02d661c3d89456ef","ec8563aa69a61bbd","eb16e8047bf1330f"],"x":734,"y":3799,"w":892,"h":222},{"id":"3a2d7a1c82fdcead","type":"trigger","z":"b5a44a6b8fa524cc","g":"c3ff746c4dd5d8a3","name":"30 seconds","op1":"","op2":"","op1type":"nul","op2type":"pay","duration":"30","extend":true,"overrideDelay":false,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":1235,"y":3840,"wires":[["02d661c3d89456ef"]],"icon":"node-red-contrib-bigtimer/timer.png","l":false},{"id":"6bc937df4c81a157","type":"switch","z":"b5a44a6b8fa524cc","g":"c3ff746c4dd5d8a3","name":"Open/Closed","property":"payload.value","propertyType":"msg","rules":[{"t":"eq","v":"open","vt":"str"},{"t":"eq","v":"closed","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":990,"y":3900,"wires":[["3a2d7a1c82fdcead","4b50fa847e306f12"],["36ca26d642338046","fe8e5cd980ce8d89"]]},{"id":"fe8e5cd980ce8d89","type":"change","z":"b5a44a6b8fa524cc","g":"c3ff746c4dd5d8a3","name":"RESET","rules":[{"t":"set","p":"reset","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":1235,"y":3880,"wires":[["3a2d7a1c82fdcead"]],"icon":"font-awesome/fa-refresh","l":false},{"id":"4b50fa847e306f12","type":"change","z":"b5a44a6b8fa524cc","g":"c3ff746c4dd5d8a3","name":"RESET","rules":[{"t":"set","p":"reset","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":1235,"y":3940,"wires":[["36ca26d642338046"]],"icon":"font-awesome/fa-refresh","l":false},{"id":"36ca26d642338046","type":"trigger","z":"b5a44a6b8fa524cc","g":"c3ff746c4dd5d8a3","name":"30 seconds","op1":"","op2":"","op1type":"nul","op2type":"pay","duration":"30","extend":true,"overrideDelay":false,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":1235,"y":3980,"wires":[["02d661c3d89456ef"]],"icon":"node-red-contrib-bigtimer/timer.png","l":false},{"id":"9930e99214c0ce15","type":"debug","z":"b5a44a6b8fa524cc","g":"c3ff746c4dd5d8a3","name":"","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":1530,"y":3940,"wires":[]},{"id":"02d661c3d89456ef","type":"junction","z":"b5a44a6b8fa524cc","g":"c3ff746c4dd5d8a3","x":1400,"y":3900,"wires":[["9930e99214c0ce15","eb16e8047bf1330f"]]},{"id":"ec8563aa69a61bbd","type":"no-op","z":"b5a44a6b8fa524cc","g":"c3ff746c4dd5d8a3","name":"Input","x":810,"y":3900,"wires":[["6bc937df4c81a157"]]},{"id":"eb16e8047bf1330f","type":"no-op","z":"b5a44a6b8fa524cc","g":"c3ff746c4dd5d8a3","name":"Output","x":1540,"y":3860,"wires":[[]]}]

With your solution I can suppress the voice response completely if, for example, I open and close the door quickly <30 seconds. I put it all in a sub flow (attached below) that works perfectly for my application, so thank you for that!

[{"id":"fcf98f412ee79100","type":"subflow","name":"flow.time","info":"","category":"","in":[{"x":160,"y":380,"wires":[{"id":"15f35af014d11b59"}]}],"out":[{"x":1260,"y":380,"wires":[{"id":"79dfadac8f91a73e","port":0}]}],"env":[],"meta":{},"color":"#C0DEED","icon":"node-red-contrib-sun-position/clock-timer-black.svg","status":{"x":500,"y":200,"wires":[{"id":"9f9bf40dd470511a","port":0}]}},{"id":"79dfadac8f91a73e","type":"junction","z":"fcf98f412ee79100","x":1180,"y":380,"wires":[[]]},{"id":"4e75361f0a901ccc","type":"junction","z":"fcf98f412ee79100","x":560,"y":440,"wires":[["fbd7f895fc7d1649","1a34d71f5baa7435"]]},{"id":"6eae251ab423fbd6","type":"junction","z":"fcf98f412ee79100","x":560,"y":320,"wires":[["fbd7f895fc7d1649","5403d70877f4991b"]]},{"id":"843d83cf9799160b","type":"junction","z":"fcf98f412ee79100","x":1000,"y":380,"wires":[["051a4af02423a941"]]},{"id":"5403d70877f4991b","type":"junction","z":"fcf98f412ee79100","x":920,"y":320,"wires":[["843d83cf9799160b"]]},{"id":"1a34d71f5baa7435","type":"junction","z":"fcf98f412ee79100","x":920,"y":440,"wires":[["843d83cf9799160b"]]},{"id":"15f35af014d11b59","type":"switch","z":"fcf98f412ee79100","name":"Open/Closed","property":"payload.value","propertyType":"msg","rules":[{"t":"eq","v":"open","vt":"str"},{"t":"eq","v":"closed","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":320,"y":380,"wires":[["6eae251ab423fbd6"],["4e75361f0a901ccc"]]},{"id":"fbd7f895fc7d1649","type":"switch","z":"fcf98f412ee79100","name":"","property":"time","propertyType":"flow","rules":[{"t":"jsonata_exp","v":"$millis() - $flowContext(\"time\") <  30000","vt":"jsonata"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":635,"y":380,"wires":[["aae3bc4e29be85e9"],["64787c1de8b159bf"]],"l":false},{"id":"64787c1de8b159bf","type":"change","z":"fcf98f412ee79100","name":"","rules":[{"t":"set","p":"time","pt":"flow","to":"$millis()","tot":"jsonata"},{"t":"set","p":"topic","pt":"msg","to":"topic","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":770,"y":400,"wires":[[]]},{"id":"aae3bc4e29be85e9","type":"change","z":"fcf98f412ee79100","name":"","rules":[{"t":"set","p":"time","pt":"flow","to":"0","tot":"num"},{"t":"set","p":"topic","pt":"msg","to":"topic","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":770,"y":360,"wires":[["2b2e6fdcc5ce87e3"]]},{"id":"9f9bf40dd470511a","type":"status","z":"fcf98f412ee79100","name":"","scope":["051a4af02423a941"],"x":220,"y":200,"wires":[[]]},{"id":"051a4af02423a941","type":"trigger","z":"fcf98f412ee79100","name":"31 seconds","op1":"","op2":"","op1type":"nul","op2type":"pay","duration":"31","extend":true,"overrideDelay":false,"units":"s","reset":"","bytopic":"topic","topic":"topic","outputs":1,"x":1095,"y":380,"wires":[["79dfadac8f91a73e"]],"icon":"node-red-contrib-bigtimer/timer.png","l":false},{"id":"2b2e6fdcc5ce87e3","type":"change","z":"fcf98f412ee79100","name":"RESET","rules":[{"t":"set","p":"reset","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":905,"y":380,"wires":[["843d83cf9799160b"]],"icon":"font-awesome/fa-refresh","l":false}]

Now, me being the curious sort that I am and you being the clever sort that you are, made me wonder if there is an easy way to make this work based on topic. Basically, instead of putting the subflow node after every sensor node, I'm wondering if there is a way to just wire all of the sensor nodes to one subflow node and have it keep track based on topic. (Currently, if I were to do that and, for example, open all of my windows, it would just skip every other window.)

In my brain, it seems like it should be doable, but I'd likely make it much more complex than it's worth.

Cheers,

Rich

Here is an example of setting up the response based on msg.topic.

[{"id":"dc4124a4.3d357","type":"inject","z":"d1395164b4eec73e","name":"","props":[{"p":"payload"},{"p":"delay","v":"3000","vt":"num"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"one","payload":"one","payloadType":"str","x":510,"y":4920,"wires":[["6577879e.0f24"]]},{"id":"6577879e.0f24","type":"switch","z":"d1395164b4eec73e","name":"","property":"time","propertyType":"flow","rules":[{"t":"jsonata_exp","v":"$millis() - $flowContext(\"time.\" & $$.topic) < delay","vt":"jsonata"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":640,"y":4920,"wires":[["c62865e.8bb9798"],["9a405fcf.440928"]]},{"id":"3f81275caaca1f95","type":"inject","z":"d1395164b4eec73e","name":"","props":[{"p":"payload"},{"p":"delay","v":"3000","vt":"num"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"two","payload":"two","payloadType":"str","x":510,"y":4960,"wires":[["6577879e.0f24"]]},{"id":"c62865e.8bb9798","type":"change","z":"d1395164b4eec73e","name":"","rules":[{"t":"set","p":"time[msg.topic]","pt":"flow","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":4880,"wires":[["dcd8ea98.f4e16"]]},{"id":"9a405fcf.440928","type":"change","z":"d1395164b4eec73e","name":"","rules":[{"t":"set","p":"time[msg.topic]","pt":"flow","to":"$millis()","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":4960,"wires":[[]]},{"id":"dcd8ea98.f4e16","type":"debug","z":"d1395164b4eec73e","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":940,"y":4820,"wires":[]}]

If you regularly change topics delete the flow context time var, this is so the context var does not become bloated with old topics.

1 Like

Very cool thanks for taking the time to satisfy my curiosity. Just a note for anyone who tries this, it works perfectly as long as the topic has no spaces in it. The spaces will generate an error:

In my case, I'm using Hubitat. What's happening is when you create a Hubitat node it autofills the node name with the device name from Hubitat. Then the topic is created from the node name. So, if the device name has spaces and you don't change the node name, then the resulting topic will throw the error.

In this example, changing the node name to something like "Sliding_Glass_Door" will eliminate the error. Of course for me, on account of Murphy's Law and all, changing the node name breaks other stuff down stream. Notwithstanding, this is a pretty slick method.

This is easily fixed by changing topic before and after like this -

[{"id":"8e3067222da0dbc6","type":"inject","z":"87795ba7e17af83e","name":"","props":[{"p":"payload"},{"p":"delay","v":"3000","vt":"num"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"one with space","payload":"one","payloadType":"str","x":490,"y":640,"wires":[["sanitize_topic"]]},{"id":"e91212fd46c2d697","type":"inject","z":"87795ba7e17af83e","name":"","props":[{"p":"payload"},{"p":"delay","v":"3000","vt":"num"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"two two","payload":"two","payloadType":"str","x":510,"y":700,"wires":[["sanitize_topic"]]},{"id":"sanitize_topic","type":"function","z":"87795ba7e17af83e","name":"Sanitize Topic","func":"msg.originalTopic = msg.topic;\nmsg.topic = msg.topic.replace(/\\s+/g, '_');\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":720,"y":680,"wires":[["a32de1ae7af3d667"]]},{"id":"a32de1ae7af3d667","type":"switch","z":"87795ba7e17af83e","name":"","property":"time","propertyType":"flow","rules":[{"t":"jsonata_exp","v":"$millis() - $flowContext(\"time.\" & $$.topic) < delay","vt":"jsonata"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":890,"y":680,"wires":[["0a179c051d1a1729"],["38d224d7d48cb14e"]]},{"id":"0a179c051d1a1729","type":"change","z":"87795ba7e17af83e","name":"","rules":[{"t":"set","p":"time[msg.topic]","pt":"flow","to":"0","tot":"num"},{"t":"set","p":"topic","pt":"msg","to":"originalTopic","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1120,"y":660,"wires":[["c1044222bebb6cbe"]]},{"id":"38d224d7d48cb14e","type":"change","z":"87795ba7e17af83e","name":"","rules":[{"t":"set","p":"time[msg.topic]","pt":"flow","to":"$millis()","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1150,"y":740,"wires":[[]]},{"id":"c1044222bebb6cbe","type":"debug","z":"87795ba7e17af83e","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1350,"y":620,"wires":[]}]
1 Like

It sure does. Thanks!

1 Like