Hi all, I have been meaning to give back to this forum for ages, this small project feels like the ideal first project to share!
I setup Node-Red and Home Assistant in my dad's house a few years ago. He loves it but he stays away from anything 'complicated'
I've been abroad for over a year, and so when I visited him recently he had a lot of ideas which he'd like me to implement.
One of those was the bathroom extractor fans. We had initially fitted humidity sensors connected to Shelly's in the bathrooms, but these proved to be quite unreliable. He instead fitted flow switches onto the water feed, and used these to trigger a fixed 45 min timer.
The problem is, a 5 min shower would cause the fan to run for 40 mins after finishing, and if someone had a 30 min shower, the fan would only run for an additional 15 mins.
Sure, this could be resolved by starting the timer when the flow turns off, but that would still mean a 5 min shower runs the fan for a total of 50 mins!
He suggested a variable timer which would work by first timing the length of the shower, and then using that time as the OFF delay.
Sounds great! And easy. So I spent 10 mins writing a simple function to accomplish the task. I took a shower, momentarily turned it off when I meant to change the temperature, carried on and... the fan stopped around 2 mins after I started!
The timer did it's job, but because I turned the shower back on whilst the fan was running, the initial timer switched it off.
That lead to a much longer length of time to properly optimise my function.
It now has variables for 'minimum time' which is the shortest possible time the fan will run for - if set to 10 mins and you have a 1 min shower, the fan will run for 10 mins after you finish. If you would take a 15 min shower, it would turn off 15 mins after you finish.
I also implemented an adjustment variable, which multiplies
the timer length. For example if it's set to 1.2 and you take a 10 min shower, the fan will turn off 12 mins after you finish.
the main thing, is that if a shower is finished but the fan is still running, a new shower will not over rule the existing timer. At the end of the second shower, the function will compare both timers, and it will keep the fan running for the longest of the two options.
I may implement a 'max timer time' which would restrict the timer to a maximum time, but for now I think it's okay.
The flow looks empty, but all is inside the function. I have commented this in a way which should make it clear to others. I hope you too find it useful!
Thanks for reading
flows.json (3.4 KB)
[{"id":"cce653c17c864884","type":"inject","z":"17ceb0cadd62d6c7","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"test","payload":"true","payloadType":"bool","x":200,"y":300,"wires":[["5d06fc35a797bf19"]]},{"id":"5d06fc35a797bf19","type":"function","z":"17ceb0cadd62d6c7","name":"fan timer","func":"var currentTime = new Date().getTime();\nvar topic = msg.topic;\nconst startStore = `${topic}StartTime`; //template literal variables for storage\nconst endStore = `${topic}EndTime`;\nconst delayStore = `${topic}DelayTime`;\nvar previousEndTime = flow.get(endStore); //a variable for the previous end time\nvar delayTime = 1; //1ms default to keep the delay timer happy\n\n// \\/ \\/ USER ADJUSTABLE VARIABLES BELOW \\/ \\/\nconst timeAdjust = 1.2 // how much to multiply the time by. DON'T SET TO ZERO! examples outputs from a 10min input: 0.5 = 5mins 1 = 10mins 1.5 = 15mins etc\nvar minimumDelay = 1 // The shortest off delay possible (in mins). If shower runs for less than this time, the delay will be extended to this number\n// /\\ /\\ USER ADJUSTABLE VARIABLES ABOVE /\\ /\\\n\nminimumDelay *= 60000 // convert into ms\n\nif(msg.payload === true){ // when flow switch turns on\n if(currentTime < previousEndTime){ //if the switch has turned on before the previous timer has elapsed\n msg.reset = true // void the existing timer\n }\n flow.set(startStore,currentTime); //save the start time.\n}\n\n\n\nif (msg.payload === false){ // when flow switch turns off\n var startTime = flow.get(startStore); //retreive the previous starting time\n delayTime = currentTime - startTime //calculate the amount of time between start and stop\n delayTime *= timeAdjust; //multiply the delay by the adjustment number\n if(delayTime < minimumDelay){ //if the delay is below the minimum threshold\n delayTime = minimumDelay //then increase the delay to the minimum required\n }\n \n}\n\nvar endTime = currentTime + delayTime; //create a variable with the actual timer end time.\n\n// If the previously calculated end time is later than the newly calculated end time, \n// it means the new timer would cut the time short. \n// Therefore, we update the end time to be the later one.\nif (previousEndTime > endTime){\n endTime = previousEndTime;\n delayTime = endTime - currentTime // update the delay timer.\n}\n\n\n\nflow.set (endStore,endTime) //save the OFF time in a variable\nmsg.delay = delayTime;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":320,"wires":[["7a2aedb83e5733a5"]]},{"id":"05ae2bc7d2738f70","type":"inject","z":"17ceb0cadd62d6c7","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"test","payload":"false","payloadType":"bool","x":200,"y":340,"wires":[["5d06fc35a797bf19"]]},{"id":"7a2aedb83e5733a5","type":"trigger","z":"17ceb0cadd62d6c7","name":"","op1":"","op2":"","op1type":"nul","op2type":"pay","duration":"1","extend":false,"overrideDelay":true,"units":"s","reset":"","bytopic":"topic","topic":"topic","outputs":1,"x":520,"y":320,"wires":[["9a51b6bd97b62214"]]},{"id":"9a51b6bd97b62214","type":"debug","z":"17ceb0cadd62d6c7","name":"","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"delay","statusType":"msg","x":670,"y":320,"wires":[]}]
Admin edit: removed block quote and surrounded JSON flow with three backtick code fence ```