A complicated bathroom fan timer

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 :slight_smile:

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 ```

1 Like

Note: I changed the topic to Share your Projects, rather than nodes :grinning:

I would also not that your Block Quote flow json is corrupt due to not posting it according to forum guidelines

In order to make code readable and usable it is necessary to surround your code with three backticks (also known as a left quote or backquote ```)

``` 
   code goes here 
```

You can edit and correct your post by clicking the pencil :pencil2: icon.

See this post for more details - How to share code or flow json

:grin: A bathroom extractor fan only costs a few Ā£/$ a year to run FULL TIME even with the super expensive power we have in the UK so really the cost of keeping a Shelly unit powered 24/7 quite possibly outweighs any savings from turning off the fan a few minutes sooner. Certainly the cost of buying the Shelly would take years to recoup.

Though, of course, running the fan permanently would probably be annoying and the fan would probably die after a decade or so. :rofl:

Fun project though.

Not sure your maths is correct here a 20w fan running FULL TIME would cost approx Ā£40 a year. (may be by full time you do not mean 24/7, then it may be Ā£4 a year on timer and humidity) A shelly would use approx Ā£2 a year to run as they generally use <1W an hour approx 9Kwh a year

But I do agree that I would use a humidistat/timer fan and let it run itself, no need for a smart device here. But there may be a place for it if noise at night was an issue.

1 Like

If all of the bathroom fans in the UK were to turn off 5 minutes earlier it would make a much greater contribution to saving the planet than if all of the diesel cars were scrapped and replaced with electric.

Think global, act local.

Maybe you could turn a few of your Pi's off, that would help to. Talking to all PI addicts, not just jbudd. (JK)

Ouch! It's not like there's dozens of them!

You are right of course @E1cid.

I do economise by running Node-red on 4 Pi Zero 2Ws rather than 1 or 2 Pi 5s. or even more extravagant, a PC.

But running one less Pi Zero 2W saves 5 - 10 kWh a year and I'll look into which ones I can turn off. :innocent:

Hmm theres the one I reserve for pasting/creating flows from this forum.
Theres two different database servers.
I wonder what #4 is for...

2 Likes

Thanks. I seem unable to edit my own posts. Assuming it's because I'm not a trusted member or something?

Thanks for fixing it for me :slight_smile:

The reason for this was not due to any cost saving. The house has ample solar so that's not really an issue.

The reason is because, even with a 'silent' in line fan hidden in the roof space, it can still be heard faintly when on. He had the idea, and I implemented it.

Also, the house was rewired around 5 years ago. Every single switchable device is on some sort of controllable device be it shelly, modbus relay banks, or other methods, so this project didn't add any extra hardware.

I did not fix it, looks like it was @Steve-Mcl.
Do you have an edit button below the post, show us an image, as you should be able to edit your own post.

I use a Shelly and a TH sensor connected to openHAB. The key is the dewpoint not the humidity. High humidity and high temperature (in summer) are fine but not in winter. Humidity sensors are also notoriously inaccurate (but fairly repeatable). I ran a bath and waited until condensation started to appear on the tiles and set the on trigger to 0.5C ABOVE the dewpoint reading. You need to do something similar to work out the switch off point (which should just be the dew point). The triggers are based on temperature in relation to dewpoint (not the dewpoint itself) - ie. when the temperature is close to the dewpoint. It's also better to start the fan 'early' rather than late. Also the sensor should be away from the source of the steam

My Shelly1 with the TH adaptor cost less than Ā£20. A 4" 12W Xpelair fan will consume 105kWh per year if on permanently - a 6" fan might be double that.. A Shelly will consume less than 1/10 of that - do the maths as they say.

The original post was rather meant tongue-in-cheek but it seems to have taken a life of its own with talk about comparing to diesel cars and such. There is a lot of home automation that will not really pay for itself - doesn't stop it being fun to do though.

1 Like

When you live in a cold country, the bathroom extractor fan dumps warm air outside and for every liter of air dumped, you need to replace it with cold air from the outside so the cost for running the fan is quite a bit different from the power consumed by the fan itself... in addition to noise and general wear if the fan of course.

I have had several different ways to control the fan but the one that I'm using now and the one I'm most satisfied with is this one:
Humidity is sensed by Aeotec Multisensor. The value is lightly filtered to represent the current humidity level and it is also heavily filtered to generate a reference value. The difference between the two values decide weather the fan should run or stop. My fan is fairly small so I let it run for an additional 45 minutes after the humidity is normal to make sure that condensation on the walls also is vented out. All values is stored in flow variables. This flow ends up in a boolean variable to control the fan on/off.

Humidity values for 2 days, one shower taken in the period:
image

[{"id":"833e0c55bf372688","type":"comment","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"Fukt og Avtrekk Bad","info":"","x":240,"y":3990,"wires":[]},{"id":"1b0301f7645fbd79","type":"delay","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"","pauseType":"delay","timeout":"45","timeoutUnits":"minutes","rate":"1","nbRateUnits":"1","rateUnits":"minute","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":true,"allowrate":false,"outputs":1,"x":820,"y":4120,"wires":[["d2ce6eb7073dbc30"]]},{"id":"06fbe131e08b90fa","type":"function","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"Reset delay","func":"//msg.delay = 600000;\nvar m1 = {reset:true};\nreturn [[m1,msg]];","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":640,"y":4150,"wires":[["1b0301f7645fbd79"]]},{"id":"8e86f81aa403915f","type":"mqtt in","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"Beveg Bad Aeotec Fukt","topic":"zwave/Bad/BevegAeotec/sensor_multilevel/endpoint_0/Humidity","qos":"2","datatype":"json","broker":"6db118ed1b0c56de","nl":false,"rap":true,"rh":0,"inputs":0,"x":250,"y":4030,"wires":[["d35991c189b1b905"]]},{"id":"17f5cff74199abc0","type":"function","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"Hygrostat Bad","func":"var FuktBad = flow.get(\"FuktBadFilt\")||30;\nvar FuktBadRef = flow.get(\"FuktBadRef\")||99;\nvar SW = false;\nif((FuktBad - FuktBadRef)>15){SW=true}\n\nvar svar = {payload:SW};\nreturn svar;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":230,"y":4130,"wires":[["dc6a98af255839c0"]]},{"id":"55352dc78882c3df","type":"change","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"flow:FuktBad","rules":[{"t":"set","p":"FuktBad","pt":"flow","to":"payload.value","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":4030,"wires":[[]]},{"id":"e60e899ca69bc4d5","type":"switch","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"Fukt Bad","property":"payload","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"true","repair":false,"outputs":2,"x":440,"y":4090,"wires":[["986fbd6f0230ea25","ba1a913816408dee"],["06fbe131e08b90fa"]]},{"id":"dc6a98af255839c0","type":"rbe","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":430,"y":4130,"wires":[["e60e899ca69bc4d5"]]},{"id":"986fbd6f0230ea25","type":"change","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"Stop delay","rules":[{"t":"set","p":"reset","pt":"msg","to":"1","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":640,"y":4110,"wires":[["1b0301f7645fbd79"]]},{"id":"d04eb2041f306fe5","type":"function","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"Filtrer fukt bad","func":"var fuktBadFilt = flow.get(\"FuktBadFilt\")||30;\nvar fuktBadRef = flow.get(\"FuktBadRef\")||30;\nvar fuktBad = flow.get(\"FuktBad\")||30;\n\nfuktBadFilt = (((fuktBadFilt * 9) + fuktBad)/10);\nfuktBadRef = (((fuktBadRef * 2999) + fuktBadFilt)/3000);\n\n// It don't make sense to have ref higher than real value\nif (fuktBadFilt<fuktBadRef) fuktBadRef = fuktBadFilt;\n\nflow.set(\"FuktBadFilt\",fuktBadFilt);\nflow.set(\"FuktBadRef\",fuktBadRef);\n\nvar svar = {payload:fuktBadFilt};\nreturn svar;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":230,"y":4170,"wires":[["411e9369c029a27b","4fa5875183c62bff"]]},{"id":"e47327bd829e59aa","type":"inject","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"15s","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":4090,"wires":[["d04eb2041f306fe5","17f5cff74199abc0"]]},{"id":"d35991c189b1b905","type":"show-value","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"","path":"","x":460,"y":4030,"wires":[["55352dc78882c3df"]]},{"id":"ba1a913816408dee","type":"change","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"","rules":[{"t":"set","p":"AvtrekkFukt","pt":"flow","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":1030,"y":4090,"wires":[["e402cc9f74a0d966"]]},{"id":"d2ce6eb7073dbc30","type":"change","z":"6e716fb5.71658","g":"5a56059f0bacfbe9","name":"","rules":[{"t":"set","p":"AvtrekkFukt","pt":"flow","to":"false","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":1030,"y":4120,"wires":[["e402cc9f74a0d966"]]},{"id":"6db118ed1b0c56de","type":"mqtt-broker","name":"DaleMQTT","broker":"172.16.0.94","port":"1883","clientid":"34567890","autoConnect":true,"usetls":false,"protocolVersion":"5","keepalive":"60","cleansession":false,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""}]
2 Likes