Timers stored in context fire mqtt payloads when triggered

I have a few problems I'm trying to solve.

First, not very common but does happen. For fans, lights, etc I have timers to auto shut them off. Think motion lights, bathroom fan, etc. Humidity is too high bathroom fan comes on for 10 minutes and then off. Sometimes if I'm testing something I will restart NR during these timers (happens with motion lights more often) and they will then be reset and won't trigger. It's not a huge issue but fans/lights have been left on for a while because of this.

Second, related to the first, the logic around what happens if the fan/light is manually shut off gets complicate because then the timer triggers later and might shut off prematurely. Light is triggered on because of motion, auto off in 5 minutes. I shut the light of manually in 4 minutes, then back on and a minute later the timer fires and auto shuts the light off. I have some logic in place to get around this but it's ugly.

Third, climate. I control my heatpumps via mqtt and I want to implement a hold for n hours. I could use a timer but again to my first point if I restart etc it might get reset and will never go off. It would also be nice to easily cancel this hold or change it, again, complicate NR logic to handle these case.

I've been searching for a solution but nothing has really popped up. In my ideal situation the timer (the date) would be stored and something would check every minute? if that date has passed simply trigger the event. This would easily allow me to remove, replace or catch missed events.

So, picture a state object like this

[
 { id: $unqiue_set_by_setter, trigger: $timestamp, topic: timer/fire/test },
 { id: living_climate_hold, trigger: $timestampe2, topic: climate/hold/off }
]

Every minute I loop over that list and check if it's past the timestamp. If it is I publish to the stored topic (with optional payload). Something else, probably another flow, would listen for that topic and handle the event (shut off the fan). If the light is shut off I simply remove the item from the list. If it's updated (hold changed for heatpump) I simply update the object in the list.

Thoughts? Am I overly complicating this?

Truth is, that reliable system can not be built up only on top of timers and server side switch commands. Knowledge of true state of devices the server has to control is mandatory.
But you haven't told us anything about how and what kind of the communication is set up between devices and node-red. By knowing nothing about this, it is hard to advise anything.

1 Like

I would ignore the shutoff signal and handle the timer entirely in Node-RED. That would let you reset/cancel the timer when a manual command is received.

mqtt is the communication method between devices/nodes. So, here's a scenario

  • Light turns on (Zwave, triggers state change in HA)
  • NR sees this state change through HA and sends an mqtt msg topic: timers/enable payload: {id: "mudroom_light", datetime: "2019-10-4....", "fire_topic": "timers/mudromlight/fire" } *

  • NR node listening on mqtt timers/enable sees new payload and stores the entire payload in the context
  • NR inject node is setup to fire every minute. It fires, pulls out the context list and loops over each one to see if the datetime is past the current datetime. If it is, it fires off an mqtt with all the data in the stored payload, so in this exampl topic: timers/mudromlight/fire, payload: {id: "mudroom_light", datetime: "2019-10-4...." }

  • NR node is setup to listen for timers/mudromlight/fire and issues calls the service to shut the mudroom light off.

In this scenario, the internal bullets are handled but an entire "timer node", so when you want to use it you have to setup only two nodes. First node is used to fire the initial timers/enabled msg with the payload when the light is turned on and the second node will listen for the timers/mudroomlight/fire msg and perform the action you want to happen when the timer is triggered.

In this scenario, if the server was restarted etc the timer would still fire the next time the inject node ran. Also, sending the same command again would override the existing item in the context so I wouldn't have to worry about stopping the old on. Removal is just as easy, light shuts off just fire a message to remove that id from the list.

@hotNipi so few people understand this concept, even at my workplace, timers are rife. They are (to quote the Waterboys mother), the devil.

Sure they work, sure they do a job for you. Sure 99% of the time there is no issue, then they bite.

Nothing beats true feedback and interlocking. Nothing.

I've seen professional (highly paid plc programmers) "fix" a problem with asynchronous data with a "settle timer". It boils my p*ss!

Edit...
Rant over. I feel better now. :slight_smile:

1 Like

@Steve-Mcl OK, but HOW do you do that?
Can you point us to a real example / flow / tutorial?

I'm trying to solve a similar problem: link...

So how do you turn on the fan? A relay, SSR, or some other mechanism? I'm pondering the feed back portion of your dilemma as posed by @Steve-Mcl.
[Edit] I'm not sure why you would need an interlock or what purpose it would serve though

Mulling this over for a while I can see the viewpoints where feedback and interlocks would be nice. However, this isn't a PLC controlled industrial machine so while I respect the opinions about the feedback and interlock I think that might be a little overkill for residential node red applications. Having said that I'm glad @Steve-Mcl did get that off his chest and feels better.
Specifically addressing your heater application where you want to run for N hours then make sure it shuts off. I slapped together a little code that I think might do the job (minus feedback and interlock of course)

[{"id":"ef8cf370.ab32a","type":"inject","z":"41fec104.9b49b8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":170,"y":140,"wires":[["4c978111.f3d3e8"]]},{"id":"4c978111.f3d3e8","type":"function","z":"41fec104.9b49b8","name":"","func":"let stopTime = msg.payload + 60000; //set to 14400000 for 4 hours\n\nflow.set('stopTime',stopTime);\n\nz = 14400; // timer runs in seconds \n\nmsg = {\n    \n}\n\nmsg = {\n    payload: 'on',\n    timeout: z, // need to change to stopTime\n    warning: 0,\n    }\n\nnode.status({fill:\"blue\",shape:\"dot\",text:stopTime});\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":340,"y":140,"wires":[["2f71649e.c4037c"]]},{"id":"9250cfc5.3524c8","type":"inject","z":"41fec104.9b49b8","name":"5 seconds","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":true,"onceDelay":0.1,"topic":"timer","payload":"1","payloadType":"num","x":180,"y":260,"wires":[["13ad32f3.d87695"]]},{"id":"2f71649e.c4037c","type":"mytimeout","z":"41fec104.9b49b8","name":"","outtopic":"","outsafe":"","outwarning":"Warning","outunsafe":"off","warning":"5","timer":"30","debug":false,"ndebug":false,"ignoreCase":false,"repeat":false,"again":false,"x":510,"y":140,"wires":[["e6c31aa4.fa912"],["25c1eacf.5e6d46"]]},{"id":"25c1eacf.5e6d46","type":"function","z":"41fec104.9b49b8","name":"timeout status","func":"if (msg.payload > 0){\n    msg = {\n        payload: 1,\n    }\n} else {\n    msg = {\n        payload: 0,\n    }\n}\n\n\nnode.status({fill:\"green\",shape:\"ring\",text:msg.payload});\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":700,"y":180,"wires":[["837a2967.235fe"]]},{"id":"e6c31aa4.fa912","type":"function","z":"41fec104.9b49b8","name":"other status","func":"node.status({fill:\"green\",shape:\"ring\",text:msg.payload});\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":690,"y":100,"wires":[[]]},{"id":"837a2967.235fe","type":"function","z":"41fec104.9b49b8","name":"check time","func":"let z = flow.get('stopTime');\n\nconst now = Date.now();\n\nif (now > z){\n    msg = {\n        payload: 0,\n    }\n}\n\nnode.status({fill:\"green\",shape:\"ring\",text:msg.payload});\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":910,"y":260,"wires":[[]]},{"id":"13ad32f3.d87695","type":"function","z":"41fec104.9b49b8","name":"epoch time","func":"const now = Date.now();\nnode.status({fill:\"blue\",shape:\"dot\",text:now});\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":490,"y":260,"wires":[["837a2967.235fe"]]},{"id":"917d0774.1e1ed8","type":"comment","z":"41fec104.9b49b8","name":"button to start 4 hour timer","info":"","x":210,"y":80,"wires":[]},{"id":"bad2885f.da9ec8","type":"comment","z":"41fec104.9b49b8","name":"timer to check to see if timer should be off","info":"","x":270,"y":320,"wires":[]}]


In a nutshell the code sets a time to the timer writing also to a flow.set variable, then the timer runs down and a separate timer checks to see if the time it should have turned off is past. If the time is past it turns off the output. The timer runs at 5 second intervals but you can set it to whatever granularity works for you. Doesn't answer all your questions but I believe it should help with the heater part of your situation. It doesn't address all your concerns about restarting or canceling but maybe those would be additions you could make.
Apologies if I'm totally off base here, with further apologies to @Steve-Mcl and @hotNipi

[EDIT] node-red-contrib-mytimeout is a custom node in my flow you may not have, otherwise everything else should be standard nodes