Smartening my Positive Input Ventilation unit with ESPHome!
Our Victorian house has suffered with condensation issues ever since it was modernised. (probably in the 80s, if the newspaper we found in the loft was anything to go by!)
Wet patches would appear on walls, and the windows would be dripping wet every morning causing lots of minging black mould...
It turns out that these houses were never designed to be sealed and insulated as well as they are today, and that's a major part of the problem!
As built, the house had coal fires in every room, drawing hot moist air up and out through the chimney constantly during winter.
However, with modern double glazed windows and doors, all gaps sealed up to reduce cold draughts, and central heating instead of fires, that moist air can't escape very well.
It's kinda funny, because cold air sneaks in like a friggin cat burglar!
but anyway...
To help remedy this we had a fancy Positive Input Ventilation (PIV) unit installed in the loft a few years ago.
These work by drawing fresh dry air in from the (ventilated) loft space and blowing it into the house, diluting humid air with dry and forcing it out through any available gap.
and you know what, it works pretty stinkin' well at keeping the walls and windows dry!
However, can you see a problem here?
(hint: I'm still in the middle of working on that smart heating system!)
Once again, it looks like I'm off down one hell of a rabbit hole!
So, here we GO!
Contents:
For ease of reading, and to save your sanity, here's some links to each part of this project :)
- Why are we modding it, then?
- What are we working with here?
- The Innards
- Designin' time!
- Preparation usually prevents piss poor performance
- Building the thing!
- The ESPHome-ening!
- Switchy switchy
- Decoding the Matrix!
- It's under control, I promise!
- Automatic how, now?
- Automatic Hygrostatic!
- So, does all this work?
- Making the UI pretty!
- Final thoughts
- Useful gubbins
Why are we modding it, then?
If you didn't guess, it's because it makes the house very fucking cold.
You don't need to be Sherlock to work out that blowing cold dry air from the loft into your house at a constant rate will make it cold, no matter how much you insulate it.
Also, what if the air in the loft is more moist than indoors, like when it's miserable outside? (which it often is in the UK's wettest city)
We don't want to make the problem worse!
Wouldn't it be so much better if it had temperature and humidity based control?
and, why not make it work with Home Assistant, because this is a smart home after all!
Let's get cracking then!
So, what are we working with here?
Our PIV unit (heh) is a Nuaire Drimaster 2000, made in 2015.
It's pretty dumb, but does have five 'Temperature Control' options as well as six fan speed settings, all selectable by a series of well timed button presses showing different combinations of LEDs.

However, none of these really worked for us.
The in built temperature sensor is not accurate in the slightest, and there is absolutely no consideration for the actual humidity levels.
But you can disable the sensor and directly control the fan speed.
We could do something with that!

When the fan is first turned on, the LEDs will flash in a 'knight rider' sequence for around two minutes.
Yes, that's what the manual calls it! How cute!
During this, the button can be held for a few seconds to switch between modes if needed.
So I turned on the fan, held the button until option 3 (no temp control) was selected, then let go and disconnected the power to save the settings.
On subsequent power-ons the LEDs will still flash for two minutes, but will then stabilise to show the currently selected fan speed.
At this point the button can be pushed to increment the speed. Neat!
There's also a 'Boost' option shown in the manual, enabled by shorting two pins on the control board.
Interestingly, this was an even faster setting than speed 6! Useful!
but we haven't even taken the thing apart yet, so let's see what makes it tick!
The Innards
After studying the manual and playing with the different settings, I had two ideas in mind on how to control this thing.
- Completely replace the control board with a custom ESP32 based one
- Build an ESP32 based add-on board to read the LEDs and push the button
But to decide which to go with, I needed to look inside.
So I opened up the fan, and started to disassemble it for inspection.

The control board was first out, held hostage by a couple of plastic clips.
It's powered by a Microchip PIC16 microcontroller, controlling three LEDs and an arrangement of transistors to control the fan speed.
There is a connector for the fan motor, 24v DC power, and one for an external sensor which can be shorted to enable the boost mode.
I saw that 4-pin fan connector, and immediately thought "AHA! It's PWM!"
However, I was sorely mistaken.

With the board connected to power, I used my multimeter to probe the fan terminals and find the power pins.
I then connected the fan to 24v, but unlike a PWM fan, it didn't start to spin.
It only rocked backwards and forwards, never making a full rotation.
Shit.
It turns out that the fan speed is controlled by applying a varying AC voltage between the middle pins, not a PWM signal. (I do not yet own an oscilloscope, so I think this is the case...)
This was further confirmed by getting a nasty zap from the fan connector whilst holding the PCB, oopsie!
At least it wasn't a flyback transformer, those hurt!
Fuck it, LED reading button poker it is then!
I've already spent enough time recently designing replacement PCBs for fans*, and I really didn't feel like messing with weird fan motor drivers when there's other stuff to be done.
*(hint hint at a future project post!)
Once my fingers had recovered from being externally driven, I measured the LED logic levels from the PIC chip.
5v, before the resistors for the LEDs.
Further testing revealed that the boost mode is enabled by shorting pin B on the 'Sensor' connector to ground (which pin A is!), and that the button shorts a PIC GPIO to ground.
Sweet, I can do that too!
So with option 2 firmly chosen, let's get on to designing the thing!
Designin' time!
Our add on board needs to be able to read the status of the original fan board, and be able to control all of its' functions.
We need to be able to push the button, enable boost mode, switch the power on/off, and read those LEDs.
Preferably as cheaply as possible, using whatever scavenged parts I had laying around!
So with that in mind, I cooked up a rough circuit design in the notebook of dreams...

So, what do we have here?
- Smol SPST reed relays to actuate the button and boost switch
- A chonkier standard SPDT relay is used to switch the power
I'm using the normally closed contact, so the fan is on unless I turn it off - Optocouplers to convert the 5v logic level of the LEDs to 3.3v for the ESP
- An LED to show the status of the ESP's wifi
- A cheapo buck regulator module to provide 5v for my ESP from the 24v input
- A ribbon cable connected to strategic points on the fan PCB for all the signals I'll need
You may be thinking "that circuit diagram makes no sense!", and I hear you.
It was drawn late at night after some brewskis, and is missing a few things.
so let's do a proper one in Kicad!
That's better!
but, what's the difference?
The first major one is that the power relay is now driven via an NPN transistor.
ESP GPIO outputs are not strong enough to drive a proper coil relay, so I added the transistor to switch it.
However, the reed relays didn't need this, as they're specifically designed to run from a microcontroller. Neato!
I did add flyback protection diodes to the reed relays though, to protect the ESP from being cooked when they turn off, and also a power LED to show that it's on.
Also, LED eyes?
You'll see :)
We now have a design, so let's get on to building the thing!
Preparation usually prevents piss poor performance!
Before we build the new board, we're going to need to do some prep work.
So, let's first raid my parts boxes for everything we need
I'll list those components here;
- One slightly dented ESP32 dev board
I used a clone of the Wemos D1 Mini ESP32, as they're small and I already had five - A little buck converter module, recycled from a previous project
- Three PC817 optocouplers
salvaged from an old UPS, which had suffered a catastrophic transformer meltdown after hooking up a 12v boat battery in place of the original... - One 3.3v SPDT relay
again from that UPS, because free parts :) - Two ECE EDR201A500 5v SPST DIP reed relays
I already had these on hand from years ago, and they run quite happily all the way down to 1.8v - A BFY51 NPN transistor
again something I've had knocking about for years, I can't even remember why I bought these - Two LEDs, once again from the UPS
It didn't die in vain! - Two 1N4148 diodes
- One 1N4002 diode
- Some 270ohm resistors
- 8-pin, 4 pin and 3-pin ribbon sockets and plugs from the UPS
- A barrel jack connector and plug for the power
- Some rainbow ribbon cable that was laying around
- Assorted bits of wire and cable to connect things up
With all of the gubbins located, it was time to get things started!
I soldered some of the ribbon cable to the original PCB, to connect things the add-on board would need. I've labeled those up in red :)

Copious quantities of hot snot were then added to keep the thin wires attached.
A hole was then drilled in the fan casing, and the new shiny power jack was fitted into it.

Originally the power jack was on the board, with the square case nubbin shown above grabbing on to the strain relief of the plug.
However, as I'm now using a relay to switch the power, I have added this new jack to keep things tidy :)
This also meant that the original power supply plug would need some modification, to fit completely inside the case and connect to the new add-on board.

To allow this, around 30cm of the power supply cable was snipped off to be soldered to a new 3-pin connector, as shown on this lovely diagram :)
I then test-fitted the board back inside the case to make sure everything fit.

You can see here that I also removed part of the strain relief on the original plug, so it could be routed nicely inside the case.
Satisfied that everything would fit, I then soldered an 8-pin connector to the signal ribbon, and a 3-pin to the power wires :)

A new matching jack plug was then soldered to the remainder of the power supply cable, and with that the prep was mostly out of the way!
But there's one important thing missing!
The googly eye status lights!!
Wires and connectors were soldered to the LEDs, a couple more holes were drilled, the status LEDs set inside the eye holes with more hot snot, then some googly eyes were added on top :)

GLORIOUS!
So now we're done with the prep, let's get onto...
Building the thing!
Finally, it's time to get that board built!
Now, you may have noticed previously that I didn't make a PCB layout for this add-on board.
This is because I'll be using stripboard!

Stripboard is great for one-off projects and prototypes, because it's dirt cheap and much faster than waiting for a PCB from Shenzhen :)
It also saves the faff of messing around routing PCB traces, which I greatly appreciate!
I started off by soldering on the power supply components, so that the fan and ESP can each get their preferred flavour of electrons.

This was then connected up to the power supply, to set the buck regulator's output voltage to 5.1v for our ESP. (equivalent to USB)
However this is when I discovered that earlier I'd made a bit of a whoopsie...
I'd reversed the polarity on the power supply's new plug!
Naturally I only discovered this after absent-mindedly plugging my Pinecil into it to check the power supply worked.
It went bang. Bollocks.
Turns out a soldering iron is not a multimeter, and mine was RIGHT THERE!
Fs in chat for the Pinecil, I guess I'll get a new one then!
After a few choice words and a much-needed brew, a back up soldering iron was found (cheers Middle of Lidl!) and the regulator was set.
The rest of the components and wire links were then added to the board, and we now have something that looks complete!

This time, all of the connections were tested and verified with my multimeter, then a blob of hot snot was added to the regulator's adjustment pot to prevent it from getting knocked.
It was now time for a test fit, just to double check that all of this mess would fit into the fan casing.
So, I plugged it all in....

slotted it into the case...

and PHEW, it fits like a glove!
Looks like it's time to flash it with ESPHome and get the software working!
The ESPHome-ening!
ESPHome is a great project, perfect for this 'building your own smart home devices' malarkey!
It integrates deeply with Home Assistant which is really nice, and the docs are generally fantastic!
But to start using it on this newly smart fan, I'm going to need to flash that ESP32.
I connected the board up to my Home Assistant server using a micro USB cable, then used the ESPHome Builder add-on to flash the chip with a fresh blank configuration.
With this done, the ESP can now be programmed over wifi!
meaning, it's time to finally close up that case!

Isn't he gorgeous!
Switchy switchy
The next step was to write the configuration to get those relays and LED eyes working!
So, I added each of them to the config as a GPIO switch, like so;
switch:
# Make relays usable
- platform: gpio
name: "Power"
id: "PowerRelay"
icon: "mdi:lightning-bolt"
pin:
number: GPIO23
inverted: true # inverted because power relay is normally closed
restore_mode: RESTORE_DEFAULT_ON
- platform: gpio
name: "Boost"
id: "BoostRelay"
icon: "mdi:rocket-launch"
pin: GPIO18
restore_mode: RESTORE_DEFAULT_OFF
- platform: gpio
id: "ButtonRelay"
internal: true
pin: GPIO26
restore_mode: ALWAYS_OFF
# Make wifi led usable
- platform: gpio
id: "WifiLED"
internal: true
pin: GPIO19
You may notice that power relay is set as 'inverted'. Good catch!
This is because the fan power is wired through the 'normally closed' contact on the relay, meaning that the relay needs to be turned on to turn the power off.
I did it this way to ensure that the fan can still run even if the add-on board dies!
Redundancy! It's neat!
Also, what's that 'internal' all about?
The wifi LED and button relay will only ever be controlled by ESPHome, so there's no need to expose those to the Home Assistant UI.
Talking of, let's get those working!
That button relay is used to simulate me pressing the mode button on the fan itself. Therefore, there's no use in it having it be an on/off toggle...
We need to be able to press it, and we can do that with a template button!
button:
# Emulate a button press with the button relay
- platform: template
name: "Button"
id: push_button
icon: "mdi:radiobox-marked"
on_press:
then:
- switch.toggle: ButtonRelay
- delay: 50ms
- switch.toggle: ButtonRelay
When this template button is triggered, this will switch the button relay on for 50ms, then off again, recreating a physical press of the button!
Sweet!
Now, let's sort that wifi indicator...
To make this work, I'll add a couple of automations to the wifi config block.
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Turn on wifi eye LED if connected
on_connect:
- switch.turn_on: WifiLED
on_disconnect:
- switch.turn_off: WifiLED
When the wifi is connected the LED switches on, and when it isn't, it switches off. Nice!
Before we do any more though, let's test these out by saving and clicking 'install'.
After this has finished and rebooted, the wifi indicator dutifully lights up, and we now have some switches and buttons in Home Assistant!
Clicking the power results in a lovely thock from the relay, the boost switch boosts the speed, and the button changes the setting shown on the fan's LEDs!
But now, we need to find out what those LEDs on the fan are telling us.
Decoding the Matrix!
To do this, I'm going to use some of ESPHome's Duty Cycle sensors with our new board's optocouplers.
These count the amount of time an input is on and show that as a percentage, allowing us to decode the various flashing LED codes!
sensor:
- platform: duty_cycle
pin:
number: GPIO17
inverted: True # optocouplers invert the signal
mode:
input: True # set pin as input
pullup: True # pull GPIO up to 3.3v to power optocouplers
name: "LED1"
id: "LED1"
icon: "mdi:led-on"
update_interval: 100ms
filters:
- sliding_window_moving_average:
window_size: 20
send_every: 40
send_first_at: 40
- round: 0
entity_category: "diagnostic"
Optocouplers work by pulling the GPIOs down from 3.3v (HIGH) to 0v (LOW), when their internal phototransistor is switched by the LED inside.
Therefore, we need to enable the ESP's internal pull-up resistors to make those GPIOs high in the first place :)
Neat!
but, what's going on with those input filters??
I'm glad you asked!
I'm applying a sliding window moving average to the duty cycle sensors, to make their output stabilise reasonably quickly.
This will let us decode those duty cycles into statuses and speeds more accurately later on.
With three of those all added, let's save and install once again.
Then, power cycle the fan to see if they work...
They do so brilliantly, stabilising in around half a second!
Sweet!
But just knowing how much time the LEDs are on isn't going to help much.
We need to know what that actually means!
The fan shows what the current speed is by lighting three LEDs up in six different ways, and rather usefully those are printed on the case above them.

We also already know that when the duty cycles of them are roughly 35%-70%-35%, that's the 'knight rider' effect meaning it's in startup mode!
So, how do we decode that inside ESPHome?
For this I'm going to use lambdas inside some template sensors.
"but, what the fuck is a lambda when it's at home?"
Lambdas are snippets of C++ code embedded inside ESPHome's YAML configuration, which allows us to do neat tricks like evaluating if/then/else statements inside a sensor!
Let's cook some of those up then!
First up, a text sensor to show the current running state in the UI...
text_sensor:
# Show current running state of fan in UI
- platform: template
name: "Status"
id: "CurrentStatus"
icon: "mdi:power"
lambda: |-
if ((id(LED1).state <= 47) and (id(LED2).state <= 78) and (id(LED3).state <= 47) and (id(LED1).state >= 30) and (id(LED2).state >= 65) and (id(LED3).state >= 30)) {
return {"Startup"};}
else if ((id(LED1).state == 100) or (id(LED2).state == 100) or (id(LED3).state == 100)) {
return {"Running"};}
else if ((id(LED1).state == 0) and (id(LED2).state == 0) and (id(LED3).state == 0)) {
return {"Idle"};}
else {
return {"waiting"};}
update_interval: 500ms
This uses a basic if/then/else statement written in C++ to detect whether the fan is starting up, running, idle, or whether the duty cycle sensors have not yet stabilised.
If LED1 has a duty cycle of between 30% and 47%, LED2 between 65% and 78%, and LED3 roughly matches LED1, that must mean the fan is in startup!
So the code returns 'Startup' as a text string.
If any of the LEDs are static, showing a speed setting, it must be 'Running'.
If they're all off, it's 'Idle',
and if it's not sure yet, it's 'waiting'!
This sensor checks these conditions every 500ms, and returns the status to the sensor!
Neato benito :)
Next, we need to show the current speed...
text_sensor:
# Show current fan speed in UI
- platform: template
name: "Current Speed"
id: "CurrentSpeed"
icon: "mdi:speedometer"
lambda: |-
if ((id(LED1).state == 0) and (id(LED2).state == 0) and (id(LED3).state == 0)) {
return {"Idle"};}
else if ((id(LED1).state == 100) and (id(LED2).state == 100) and (id(LED3).state == 0)) {
return {"1"};}
else if ((id(LED1).state == 0) and (id(LED2).state == 100) and (id(LED3).state == 100)) {
return {"2"};}
else if ((id(LED1).state == 100) and (id(LED2).state == 0) and (id(LED3).state == 0)) {
return {"3"};}
else if ((id(LED1).state == 0) and (id(LED2).state == 100) and (id(LED3).state == 0)) {
return {"4"};}
else if ((id(LED1).state == 0) and (id(LED2).state == 0) and (id(LED3).state == 100)) {
return {"5"};}
else if ((id(LED1).state == 100) and (id(LED2).state == 100) and (id(LED3).state == 100)) {
return {"6"};}
else {
return {"waiting"};}
update_interval: 500ms
This checks for the various static LED speed codes shown on the fan case, and translates them to a readable format!
and if it's not running or not sure, it shows 'Idle' or 'waiting'
Finally, we need to combine the status and current speed into an integer, which can be used inside automations!
sensor:
# Get currently set fan speed as an integer
- platform: template
id: "RunningSpeed"
internal: True
lambda: |-
if ((id(LED1).state == 0) and (id(LED2).state == 0) and (id(LED3).state == 0)) {
return 0;}
else if ((id(LED1).state == 100) and (id(LED2).state == 100) and (id(LED3).state == 0)) {
return 1;}
else if ((id(LED1).state == 0) and (id(LED2).state == 100) and (id(LED3).state == 100)) {
return 2;}
else if ((id(LED1).state == 100) and (id(LED2).state == 0) and (id(LED3).state == 0)) {
return 3;}
else if ((id(LED1).state == 0) and (id(LED2).state == 100) and (id(LED3).state == 0)) {
return 4;}
else if ((id(LED1).state == 0) and (id(LED2).state == 0) and (id(LED3).state == 100)) {
return 5;}
else if ((id(LED1).state == 100) and (id(LED2).state == 100) and (id(LED3).state == 100)) {
return 6;}
else {
return 0;}
update_interval: 100ms
This sensor will return the fan's speed if it's running, and if it's idle or waiting it'll return 0 :)
I set this one to be internal only, as this will only ever be used inside ESPHome.
Let's save, install, and see what we get...

Success! We now know what the fan is doing!
but hang on just a tick, don't we need to actually control the speed too?
It's under control, I promise!
To make this thing actually useful, we need a way to control the fan speed.
However, the original fan board presents us with a bit of a problem...
Pressing the button will increment the speed up by 1, meaning that both higher and lower speeds need several button presses.
For example, switching from speed 2 to speed 1 is only possible by pushing that button five times!
Wouldn't it be nice to just have a slider??
So, let's add one using a template number!
number:
# Value to set fan speed to
- platform: template
name: "Speed"
id: "SetSpeed"
max_value: 6
min_value: 1
step: 1
mode: SLIDER
optimistic: True
restore_value: True
We'll use a min_value of 1, a max_value of 6, and make sure it only steps in integers (as that's all the fan supports!).
I'll also set this to save its value to flash using restore_value, and since it's not going to directly control anything, have its value be optimistic.
But, how do we set this slider's value as the fan speed then?
For that, we'll need an automation.
but it's not that simple, it seems...
The fan seems to take a few moments before the new speed is shown solidly on the LEDs, so how do we work around that?
With two binary sensors!
One to check if the running speed matches the set one, and one to make sure the fan is ready for a speed command :)
binary_sensor:
# Is the set speed equal to the current running speed
- platform: template
id: "SpeedMatches"
internal: True
lambda: |-
if ((id(RunningSpeed).state) == (id(SetSpeed).state)) {
return true;
} else {
return false;
}
# Is the fan ready to receive speed commands
- platform: template
id: "IsReady"
internal: True
lambda: |-
if (id(RunningSpeed).state > 0) {
return true;
} else {
return false;
}
I'm using lambdas here again, to easily check that both of these conditions are true!
IsReady checks if the RunningSpeed sensor we made earlier is returning a non-zero value (meaning the fan is showing its new speed), and SpeedMatches checks if the slider speed setting matches the RunningSpeed.
Got it? I believe in you!
Now we can finally use both of those to cook up an automation to sync the set speed with the fan!
I'm going to use an interval for this, with one of ESPHome's built in if/then routines;
interval:
# Sync fan speed with set speed
- interval: 2s
then:
if:
condition:
- binary_sensor.is_off: SpeedMatches # does running speed = set speed
then:
- if:
condition:
- binary_sensor.is_on: IsReady # is fan ready to receive commands
then:
- button.press: push_button
Every two seconds, this checks to see if our SpeedMatches sensor is off.
If it is, it then checks IsReady to check the fan is ready,
then it pushes the button to increment the speed!
This will repeat until the speed matches!
but once again, will this actually work?
I saved and installed, then nudged the fan speed slider from 5 to 2...

and after a few seconds of 'waiting', the current speed ticked over to 2!
Control ACHIEVED!! Fuck yeah!
but hold fire, isn't this fan supposed to be automatic?
We don't want to have to set the speed manually all the time!
and isn't it supposed to know about temperature and humidity?
Automatic how, now?
We need the fan to have a few different modes, and switch between them depending on the temperature and humidity conditions.
so, I decided on five!
- Too cold:
If the loft temperature is below 8°C, the fan should be stopped to avoid making the house cold and wasting gas for heating - Winter:
If the loft is between 8°C and 13°C, the fan should run at reduced speeds depending on the humidity, to avoid condensation.
However if the loft is more humid, it should stop to avoid making the house more humid. - Normal:
If the loft is above 13°C, the fan should run at higher speeds, again decided by humidity, to keep the house air fresh and dry.
However if the loft is more humid, the fan should run at a low speed to keep the house at least fresh. - Too warm:
If the house is above 25°C, and the loft is not cooler, the fan should be stopped to avoid making the house even hotter. - Cooling:
If the house is above 25°C, and the loft is cooler, run the fan in boost mode to cool the house down (tfw no air conditioning...)
But, how will the fan know the temperature and humidity if it has no sensors on board?
And how will it know if the loft is drier than the house??
Luckily, there are already Zigbee temperature and humidity sensors in every room, including the loft!

These are all linked to Home Assistant via Zigbee2MQTT, and have been used to control the heating and a dehumidifier :)
Fortunately, ESPHome makes it pretty easy to import sensors from Home Assistant, so let's get those added!
sensor:
# Import loft T&H and average house T&H from home assistant
- platform: homeassistant
id: "LoftTemp"
internal: true
entity_id: sensor.loft_climate_temperature
- platform: homeassistant
id: "LoftRH"
internal: true
entity_id: sensor.loft_climate_humidity
- platform: homeassistant
id: "HouseTemp"
internal: true
entity_id: sensor.average_room_temperature
- platform: homeassistant
id: "HouseRH"
internal: true
entity_id: sensor.average_humidity
I use a Home Assistant helper to calculate averages of the room temperatures and humidity, so I imported those.
As well as these, the values from the loft sensor were brought in too, all as internal only sensors.
However, we have another problem to work around...
Relative humidity isn't really useful for working out if the loft is drier than the house, because it all depends on room temperature.
Warm air can hold more moisture than cold air, so if you warm up the air its relative humidity decreases!
Once again though, it's ESPHome to the rescue!
It can calculate absolute humidity, or in other words, the amount of water in grams per cubic metre of air!
So, let's calculate the absolute humidity of both the house and the loft, using those imported temperatures and relative humidity measurements;
sensor:
# Calculate absolute humidity from temp and RH sensors
- platform: absolute_humidity
name: "Loft Absolute Humidity"
id: "LoftAH"
temperature: LoftTemp
humidity: LoftRH
- platform: absolute_humidity
name: "House Absolute Humidity"
id: "HouseAH"
temperature: HouseTemp
humidity: HouseRH
and while we're here, let's work out the difference in humidity between the house and the loft;
sensor:
# Difference between house absolute humidity and loft AH
- platform: template
id: "HumDelta"
name: "Absolute Humidity Delta"
icon: "mdi:water-plus"
update_interval: 10s
unit_of_measurement: g/m³
lambda: 'return id(HouseAH).state - id(LoftAH).state;'
This uses a simple lambda to subtract the loft absolute humidity from the house's, giving us the difference between them as a new sensor.
We'll call that our humidity delta :)
One more install and reboot later, and we have this!

Excellente!!
Now there's only a few more things we're going to need before automating it all!
I'll add five more binary sensors using lambdas, to let the fan know which mode to choose, based on the temperature.
I'm using binary sensors here to make it easier to change the temperature values in future :)
so, here they are;
binary_sensor:
# Is the loft temperature 8c or less
- platform: template
id: "LoftFreezing"
internal: True
lambda: |-
if (id(LoftTemp).state <= 8) {
return true;
} else {
return false;
}
# Is the loft temperature between 8c and 13c
- platform: template
id: "LoftChilly"
internal: True
lambda: |-
if ((id(LoftTemp).state <= 13) and (id(LoftTemp).state > 8)) {
return true;
} else {
return false;
}
# Is the loft temperature above 13c
- platform: template
id: "LoftWarm"
internal: True
lambda: |-
if (id(LoftTemp).state > 13) {
return true;
} else {
return false;
}
# Is the loft cooler than the house
- platform: template
id: "LoftCooler"
internal: True
lambda: |-
if ((id(LoftTemp).state) < (id(HouseTemp).state)) {
return true;
} else {
return false;
}
# Is the house above 25c
# used by auto mode
- platform: template
id: "HouseRoasting"
internal: True
lambda: |-
if (id(HouseTemp).state >= 25) {
return true;
} else {
return false;
}
Also, we might want to know what mode it's in, so let's add another lambda text sensor;
text_sensor:
# Show current running mode in UI
- platform: template
name: "Current Mode"
id: "RunningMode"
icon: "mdi:fan-auto"
lambda: |-
if (id(LoftFreezing).state) {
return {"Too cold"};}
else if (id(LoftChilly).state) {
return {"Winter"};}
else if ((id(LoftWarm).state) && (!id(HouseRoasting).state)) {
return {"Normal"};}
else if ((id(HouseRoasting).state) && (!id(LoftCooler).state)) {
return {"Too warm"};}
else if ((id(HouseRoasting).state) && (id(LoftCooler).state)) {
return {"Cooling"};}
else {
return {"Unknown"};}
update_interval: 5s
and with that, finally, we can get on to automating this whole shebang!!
Automatic Hygrostatic!
To automate the fan, I'm once again going to use some intervals with if/then statements.
A. LOT. OF. INTERVALS.
These will check every five minutes if the right combo of binary sensors are on, then set the fan speed according to the delta!
We'll also want to be able to disable these if needed, so I'll add another template switch right quick;
switch:
# Dummy switch for speed/power automations
- platform: template
name: "Auto Mode"
id: "AutoMode"
icon: "mdi:fan-auto"
optimistic: True
restore_mode: RESTORE_DEFAULT_ON
This will be used to decide whether the automations we're about to set up will run at all!
Next, I'll add the automation for if the loft is too cold;
interval:
# AUTO MODE
# Switch off fan if loft is freezing cold, and turn it back on if not
- interval: 5min
then:
if:
condition:
- switch.is_on: AutoMode # check if auto mode on
then:
- if:
condition:
- binary_sensor.is_on: LoftFreezing
then:
- if:
condition:
- switch.is_on: PowerRelay
then:
- switch.turn_off: PowerRelay
It checks whether auto mode is enabled, then if the loft is freezing cold.
If it is, and the fan is on, it'll turn it off.
If it isn't freezing anymore, the next automation will flick it back on!
and now for Winter Mode;
interval:
# If loft chilly, run at reduced speed according to delta unless loft is more humid
- interval: 5min
then:
- if:
condition:
- switch.is_on: AutoMode # check if auto mode on
then:
- if:
condition:
- binary_sensor.is_on: LoftChilly # check if loft chilly
then:
- if:
condition:
- sensor.in_range: # set speed 1 when delta 0-1
id: HumDelta
above: 0
below: 1
then:
- if:
condition:
- switch.is_on: PowerRelay # is this thing on
then:
- number.set:
id: SetSpeed
value: 1
else:
- switch.turn_on: PowerRelay
- number.set:
id: SetSpeed
value: 1
else:
- if:
condition:
- sensor.in_range: # set speed 2 when delta 1-2
id: HumDelta
above: 1
below: 2
then:
- if:
condition:
- switch.is_on: PowerRelay
then:
- number.set:
id: SetSpeed
value: 2
else:
- switch.turn_on: PowerRelay
- number.set:
id: SetSpeed
value: 2
else:
- if:
condition:
- sensor.in_range: # set speed 3 when delta 2+
id: HumDelta
above: 2
then:
- if:
condition:
- switch.is_on: PowerRelay
then:
- number.set:
id: SetSpeed
value: 3
else:
- switch.turn_on: PowerRelay
- number.set:
id: SetSpeed
value: 3
else:
- if:
condition:
- sensor.in_range: # turn off if loft more humid
id: HumDelta
below: 0
then:
- switch.turn_off: PowerRelay
Holy crap Batman, that's a lot of indentation!
If the loft is between 8°C-13°C and is more humid, it'll turn off the fan to save heat.
and if it's not, it'll set speed 1, 2 or 3 depending on how big the humidity delta is.
Now, Normal Mode!
interval:
# If loft warm, run at normal speed according to delta
- interval: 5min
then:
- if:
condition:
- switch.is_on: AutoMode # check if auto mode on
then:
- if:
condition:
- binary_sensor.is_on: LoftWarm # is loft warm
then:
- if:
condition:
- binary_sensor.is_off: HouseRoasting # check house not roasting
then:
- if:
condition:
- switch.is_on: PowerRelay # is this thing on
then:
- if:
condition:
- sensor.in_range: # set speed 3 when delta 0-1
id: HumDelta
above: 0
below: 1
then:
- number.set:
id: SetSpeed
value: 3
else:
- if:
condition:
- sensor.in_range: # set speed 4 when delta 1-3
id: HumDelta
above: 1
below: 3
then:
- number.set:
id: SetSpeed
value: 4
else:
- if:
condition:
- sensor.in_range: # set speed 5 when delta 3-5
id: HumDelta
above: 3
below: 5
then:
- number.set:
id: SetSpeed
value: 5
else:
- if:
condition:
- sensor.in_range: # set speed 6 when delta 5+
id: HumDelta
above: 5
then:
- number.set:
id: SetSpeed
value: 6
else:
- if:
condition:
- sensor.in_range: # set speed 2 when loft more humid
id: HumDelta
below: 0
then:
- number.set:
id: SetSpeed
value: 2
else:
- switch.turn_on: PowerRelay # if not on, make it be on
- number.set: # set speed to 3 until automation runs again
id: SetSpeed
value: 3
The indentation is strong with this one!
This checks to make sure the loft is above 13°C, but the house is not over 25°C.
then, it'll set the speed between 3-6 depending on how big the humidity delta is!
If the loft is more humid, it'll set the speed to 2 to prevent stale air.
Next up, what if the house is roasting?
interval:
# If house roasting turn off unless loft cooler, then enable boost
- interval: 5min
then:
- if:
condition:
- switch.is_on: AutoMode # check auto mode on
then:
- if:
condition:
- binary_sensor.is_on: HouseRoasting # is the house roasting
then:
- if:
condition:
- binary_sensor.is_on: LoftCooler # is loft cooler
then:
- if:
condition:
- switch.is_on: PowerRelay
then:
- switch.turn_on: BoostRelay # if on, turn on boost
else:
- switch.turn_on: PowerRelay # if off, turn on and boost
- switch.turn_on: BoostRelay
else:
- switch.turn_off: BoostRelay # if loft not cooler, turn off boost and power
- switch.turn_off: PowerRelay
else:
- if:
condition:
- switch.is_on: BoostRelay
then:
- switch.turn_off: BoostRelay
and finally, this checks to see if the house is roasting.
If the loft is warmer it'll turn the fan off, and if the loft is cooler, it'll enable boost!
But, fuck me sideways, that's a hacky way of doing things!
Doesn't matter if it works though...
So, does all this work?
After all that fuckery, this thing had better work...
WELL, here's the bit you've all been waiting for!

YES YES YES YES!!!
It works exactly as it should, varying the fan speed depending on both temperatures and humidity!
Not only that, the upper landing stays nice and warm, never dipping below 16°C.
It would help if that radiator wasn't blocked though...
And the average humidity in the house is kept nice and low!

It looks to me like we have a roaring success, and that I haven't wasted my time :)
but you know what would make it better?
A slightly nicer UI than just the devices menu...
Making the UI pretty!
I won't waste too much more of your time on this, but this isn't exactly conducive to knowing if it's working...

No problem though, let's give it a nice card in the new Sections dashboard view!

Much better!
For this I used the excellent mini-graph-card and Mushroom cards, stacked them together, then used card-mod to merge the top three entity cards together :)
Here's the YAML if you're interested!
type: vertical-stack
cards:
- type: horizontal-stack
cards:
- type: custom:mushroom-entity-card
entity: sensor.the_machine_status
name: Status
icon_color: green
card_mod:
style: |
ha-card {
border: none ;
margin: 0px -65px 0px 0px ;
}
- type: custom:mushroom-entity-card
entity: sensor.the_machine_current_speed
name: Speed
icon_color: purple
card_mod:
style: |
ha-card {
border: none ;
margin: 0px 0px 0px -10px ;
}
- type: custom:mushroom-entity-card
entity: sensor.the_machine_current_mode
name: Mode
tap_action:
action: navigate
navigation_path: /dashboard-humidity/the-machine-explained
icon_color: teal
hold_action:
action: none
double_tap_action:
action: none
card_mod:
style: |
ha-card {
border: none ;
margin: 0px 0px 0px -30px ;
}
- type: custom:mini-graph-card
entities:
- entity: sensor.the_machine_absolute_humidity_delta
name: Humidity difference
show_state: true
show_graph: false
- entity: sensor.the_machine_house_absolute_humidity
name: House
show_state: true
show_indicator: true
- entity: sensor.the_machine_loft_absolute_humidity
name: Loft
show_state: true
show_indicator: true
- entity: sensor.loft_climate_temperature
show_graph: false
show_state: true
show:
labels: true
hours_to_show: 24
card_mod:
style: |
ha-card {
border: none ;
margin: 0px 0px 0px 0px ;
}
That means there's only one thing left to do, put him back in his natural habitat!

Hanging out in the loft, using a bungee cord to avoid vibration noise being amplified by the ceilings :)
Final thoughts
So, I'll probably need to make some tweaks to the temperature and humidity tolerances, but that'll be easy enough once it's been running for a while and we have more stats.
It'd be nice to know what the other LED states are too, as apparently the fan can notify if the filters need changing as well.
I'll work that out eventually, because it sure ain't in the manual :)
For now though, this is great!
The condensation is under control and the house stays warm, 10/10!
Not bad for a side project started while working on the heating!
Fin.
WOW, you read this far?!
All 6,800 words of it?
Holy crapamoley, thank you so much for sticking with me on this mad adventure!
These projects always take much longer than one expects!
I hope you found this interesting, and maybe even useful!
Maybe this could even inspire you to start your own similar project some time :)
(and if you do, please let me know!)
There's been a few changes around the house too, since Part 1 of my heating project...
We've had our loft fully insulated, going from 80mm of rockwool to over 300mm!
Even the sloping ceilings got some love, getting 50mm of PIR insulation instead of the nothing that was there before!
2024 was a busy year and 2025 is only just getting started, with plenty more projects in the pipeline!
so maybe you should subscribe to my RSS feed, because I'm old school like that :)
Now, go get yourself a nice brew, you deserve one!

also, here's some stuff you may find useful!
Why not have a look around my website too, you may find it interesting :)