The recent heat waves made one thing very clear — a good fan is no longer an option; it is a necessity.
But if you are going to run a fan all day, why not make it actually smart?
Bladeless fans are arguably the most elegant air movers out there. Futuristic design, quiet operation, smooth, consistent airflow, and no spinning blades in sight. The only thing they are missing is a brain — and a reasonable price tag.
That is exactly what this project fixes.
The system combines several components into a single smart appliance:
- Custom-designed 3D-printed bladeless fan
- Replaceable air filter
- Infineon PSOC™ 6 microcontroller
- Two XENSIV™ DPS368 pressure sensors
- An IKEA ALPSTUGA Smart Air Quality Sensor for environmental monitoring
- Home Assistant integration through MQTT
Together, these components create a fully autonomous cooling and air purification system.
The fan adjusts its speed automatically based on room temperature, purifies the air as it moves it, and monitors its own filter health in real time by measuring the pressure difference across the filter — reporting a status from clean to replace now directly on your dashboard.
Smart, clean and cool. Let's start building.
Hardware SetupLet's start by looking at the 3D design of the fan.
3D-Printed Enclosure:
- Fan Base – Houses all the electronics and provides mounting points for the bladeless air outlet. It also contains the removable air filter compartment.
- Filter Holder – A sliding cartridge that holds the replaceable air filter. Inspired by the mechanism of old CD cases, the filter can easily be removed and replaced without disassembling the fan.
- Outer Housing– The front and rear shells of the fan are printed separately and bonded together using super glue, creating a clean and rigid enclosure.
- Pressure Sensors
To monitor the condition of the air filter, the system measures the pressure before and after the filter using two DPS pressure sensors.
One DPS sensor is already integrated on the Infineon PSOC™ 6 development board. The second external sensor must therefore be configured with a different I²C address so that both sensors can communicate on the same bus.
To change the address, simply solder the AC address selection pad on the external sensor, as shown below.
Once configured, mount the external DPS sensor inside the fan above the air filter so it measures the pressure after the air passes through the filter :)
- Ventilator
- Air Quality Sensor:
To monitor the room conditions, I used the IKEA ALPSTUGA Smart Air Quality Sensor, which measures temperature, humidity and PM2.5 (microscopic, health-hazardous air pollution particles).
Rather than communicating directly with the PSOC™ 6, the sensor connects wirelessly to the IKEA DIRIGERA Hub, which integrates seamlessly with Home Assistant. This allows the environmental data to be shared with the automation system without any additional hardware or firmware running on the microcontroller.
- Schematics
The diagram below shows how the electronics are connected.
The firmware is written in MicroPython and runs directly on the PSOC™ 6. It uses MQTT to communicate with Home Assistant through the uhome library, which makes creating Home Assistant entities from MicroPython very straightforward.
Imports & Library Installation
We first import all the MicroPython libraries needed for the project.
from machine import Pin, PWM, I2C
import network
import time
import mip
import wifi_secrets
import mqtt_secrets
import DPSThese modules allow the PSOC™ 6 to control the hardware, connect to Wi-Fi, communicate with the DPS pressure sensors, and securely store the Wi-Fi and MQTT credentials in separate files.
To make the project easier to set up, the firmware automatically installs the required MQTT and Home Assistant libraries if they are not already available on the board.
try:
from umqtt.simple import MQTTClient
except:
mip.install("umqtt.simple")
from umqtt.simple import MQTTClient
try:
import uhome
except:
mip.install("github:ederjc/uhome/uhome/uhome.py")
import uhomeThis means that after flashing the firmware, any missing libraries are downloaded automatically, so there is no need to install them manually beforehand.
Fan Settings
Next, we define the fan settings used throughout the project.
FAN_PWM_PIN = "P9_7"
PWM_FREQUENCY = 29000
MAX_DUTY = 65535
PWM_FOR_30_PERCENT = 65
PWM_FOR_100_PERCENT = 100
KICKSTART_PCT = 80
KICKSTART_MS = 1000
pwm = PWM(Pin(FAN_PWM_PIN), freq=PWM_FREQUENCY, duty_u16=0)
current_ha_speed = 0All the fan parameters are defined in one place, making them easy to adjust later if you use a different motor or motor controller.
The fan is controlled using a 29 kHz PWM signal, generated by the PSOC™ 6 and sent to the 28 V motor controller, which then drives the brushless fan.
Since brushless fans sometimes struggle to start at low speeds, I added a kick-start feature. Whenever the fan is turned on, it first runs at 80% power for one second before switching to the requested speed. This makes sure the fan starts reliably every time, even when running at low speed.
The firmware currently supports two operating speeds:
- Low speed for gentle airflow.
- Full speed for maximum cooling.
Of course, these values can easily be adjusted to suit a different fan or personal preference.
Filter Thresholds
To monitor the condition of the air filter, the firmware compares the measured pressure drop against a set of predefined thresholds.
CLEAN_BASELINE = 150
THRESHOLD_WARNING = CLEAN_BASELINE * 1.15
THRESHOLD_DIRTY = CLEAN_BASELINE * 1.30
THRESHOLD_REPLACE = CLEAN_BASELINE * 1.50
FILTER_UPDATE_INTERVAL_MS = 5000
last_filter_update = 0
STATUS_MAP = {
"CLEAN": 0,
"GETTING DIRTY": 1,
"DIRTY - Clean Soon!": 2,
"REPLACE FILTER NOW!": 3
}Instead of replacing the filter after a fixed amount of time, the fan continuously checks its actual condition by measuring the pressure difference across the filter.
A clean filter allows air to flow through easily, resulting in a small pressure drop. As dust builds up, the filter becomes more restrictive, increasing the pressure difference between the two DPS sensors.
The clean filter baseline is set to 150 Pa, based on actual measurements. From this value, three additional thresholds are calculated automatically. Depending on the measured pressure drop, the filter is classified into one of four states:
- Clean
- Getting Dirty
- Dirty – Clean Soon!
- Replace Filter Now!
The filter status is checked every 5 seconds and published to Home Assistant as a value between 0 and 3, making it easy to display on the dashboard or use in automations.
Fan Functions
Now that everything is configured, we can create the functions that control the fan.
def set_pwm(percent):
duty = int((percent / 100) * MAX_DUTY)
pwm.duty_u16(duty)
def set_fan_from_home_assistant(value):
global current_ha_speed
value = int(float(value))
if value <= 0:
current_ha_speed = 0
set_pwm(0)
elif value <= 50:
current_ha_speed = 40
set_pwm(KICKSTART_PCT)
time.sleep_ms(KICKSTART_MS)
set_pwm(PWM_FOR_30_PERCENT)
else:
current_ha_speed = 100
set_pwm(KICKSTART_PCT)
time.sleep_ms(KICKSTART_MS)
set_pwm(PWM_FOR_100_PERCENT)
print("HA speed:", current_ha_speed, "%")
fan_speed.publish(current_ha_speed)The set_pwm() function is responsible for controlling the fan speed. It converts a percentage value into a 16-bit PWM duty cycle and sends it to the PWM output connected to the 28 V motor controller.
The set_fan_from_home_assistant() function is called whenever Home Assistant sends a new speed value. Depending on the selected speed, the fan is placed into one of three states:
- 0% – Fan turned off.
- 1–50% – Low speed.
- 51–100% – Full speed.
Whenever the fan starts, it first runs at 80% power for one second before switching to the requested speed. This kick-start ensures the brushless fan always starts reliably, even at low speeds.
Finally, the current fan speed is published back to Home Assistant so the dashboard always shows the actual operating speed.
Sensor Functions
Next, we create the functions used to measure the pressure drop across the air filter and determine its condition.
def get_pressure_drop():
p_before = sensor_before.measurePressureOnce()
p_after = sensor_after.measurePressureOnce()
return p_before - p_after
def get_filter_status(drop):
if drop < THRESHOLD_WARNING:
return "CLEAN"
elif drop < THRESHOLD_DIRTY:
return "GETTING DIRTY"
elif drop < THRESHOLD_REPLACE:
return "DIRTY - Clean Soon!"
else:
return "REPLACE FILTER NOW!"The get_pressure_drop() function reads the pressure from both DPS sensors. One sensor measures the pressure before the filter, while the other measures it after the air has passed through the filter. The difference between these two readings gives us the pressure drop across the filter.
A clean filter lets the air pass through easily, so the pressure drop is smaller. As the filter collects more dust, it becomes harder for the air to pass through, and the pressure drop increases.
The get_filter_status() function compares the measured pressure drop with the thresholds defined earlier. It then returns one of four filter states:
- CLEAN
- GETTING DIRTY
- DIRTY – Clean Soon!
- REPLACE FILTER NOW!
This makes the sensor values easier to understand and allows the current filter condition to be shown directly in Home Assistant. Home Assistant receives the filter status as a number from 0 to 3:
0 means clean, 1 means getting dirty, 2 means the filter should be cleaned soon, and 3 means it should be replaced.
This is how it looks like on HA:
WiFi & MQTT Setup
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
print("Connecting to WiFi...")
wlan.connect(wifi_secrets.ssid, wifi_secrets.psk)
while not wlan.isconnected():
print(".", end="")
time.sleep(1)
print("\nWiFi connected")
print("PSOC IP:", wlan.ifconfig()[0])
connect_wifi()
device = uhome.Device("Smart Fan")
mqttc = MQTTClient(
device.id,
mqtt_secrets.broker,
user=mqtt_secrets.user,
password=mqtt_secrets.password,
keepalive=60
)
device.connect(mqttc)The connect_wifi() function activates the Wi-Fi interface and connects the board using the credentials stored in wifi_secrets.py.
Keeping the Wi-Fi details in a separate file is useful because it avoids writing private information directly inside the main program.
The code waits until the connection is successful and then prints the IP address of the PSOC™ 6. This is useful when testing or debugging the project.
Once the Wi-Fi connection is ready, a new Home Assistant device called Smart Fan is created using the uhome library.
The MQTT client then connects to the broker using the information stored in mqtt_secrets.py. MQTT is used to send the fan speed and filter status to Home Assistant, and also to receive new fan speed commands from the dashboard.
The keepalive=60 setting helps keep the connection active by checking it regularly.
Home Assistant Entities
fan_speed = uhome.Number(
device,
"Speed",
min=0,
max=100,
step=1,
unit_of_measurement="%",
mode="slider",
icon="mdi:fan"
)
fan_speed.set_action(set_fan_from_home_assistant)
filter_status_num = uhome.Number(
device,
"Filter Status",
min=0,
max=3,
step=1,
unit_of_measurement="",
mode="box",
icon="mdi:air-filter"
)
device.discover_all()
print("Ready!")The project creates two entities in Home Assistant.
The first is the Speed entity. Although it appears as a slider in Home Assistant, it is mainly controlled automatically by the temperature automation. Whenever the room temperature changes, Home Assistant updates the slider value, and the PSOC™ 6 adjusts the fan speed accordingly. The slider can also be moved manually if you want to override the automatic control for testing or personal preference.
The second entity is the Filter Status. Every 5 seconds, the PSOC™ 6 measures the pressure drop across the filter and publishes a value between 0 and 3.
Finally, device.discover_all() automatically registers both entities with Home Assistant using MQTT Discovery. As soon as the firmware starts, they appear in the Home Assistant dashboard without any additional configuration.
Main Loop
while True:
device.loop()
now = time.ticks_ms()
if time.ticks_diff(now, last_filter_update) >= FILTER_UPDATE_INTERVAL_MS:
last_filter_update = now
try:
drop = get_pressure_drop()
status = get_filter_status(drop)
print(f"Pressure Drop: {drop:.2f} Pa | Status: {status}")
filter_status_num.publish(STATUS_MAP[status])
except Exception as e:
print(f"Filter sensor ERROR: {e}")
time.sleep_ms(100)The first line inside the loop, device.loop(), maintains the MQTT connection and checks for new commands from Home Assistant. This allows the fan to react when the temperature automation changes the value of the Speed entity.
The filter does not need to be measured during every loop cycle. Instead, time.ticks_ms() and time.ticks_diff() are used to check whether the five-second update interval has passed.
When it is time for a new measurement, the program:
- Calculates the pressure drop across the filter.
- Converts it into a readable filter status.
- Prints the result for debugging.
- Uses
STATUS_MAPto convert the status into a value from 0 to 3. - Publishes this value to the Filter Status entity in Home Assistant.
The sensor measurement is placed inside a try/except block. Therefore, if one of the pressure sensors temporarily fails to return a value, the error is printed without stopping the entire program.
Finally, the short 100 ms delay prevents the loop from running unnecessarily fast while still keeping the MQTT communication responsive. This non-blocking structure is important because a five-second sleep() would prevent the PSOC™ 6 from receiving new fan commands during that time
This is where the fan becomes truly autonomous — no manual intervention needed.
The automation is built directly in the Home Assistant automation editor. No extra code, no YAML editing, just point and click.
When — Triggers:
Two triggers fire the automation:
- Whenever the temperature value changes state
- Every minute as a safety refresh — making sure the fan is always at the right speed even if a state change was missed
This is how the Home Assistance interface looks (Setting > New Automation)
Then do — Choose between 3 options:
Based on the current temperature, one of three options is executed:
- Option 1 — Temperatureabove 25.9°C → The fan turns on and the Speed entity is set to 100%
- Option 2 — Temperaturebetween 20.9°C and 26°C → The fan turns on and the Speed entity is set to low speed — just enough to keep the air moving
- Option 3 — Temperaturebelow 20.9°C → The fan turns off
These temperature limits are only an example. They can easily be changed in Home Assistant depending on the room, season or preferred comfort level. :)
DemoFarewellNow that your smart bladeless fan is ready, it's time to sit back, relax, and enjoy the breeze. Hopefully, it will help you stay cool during the next heatwave. :)
Feel free to customize the design, add more speed levels, or integrate additional sensors to make the system even smarter. If you build your own version, I'd love to see how it turns out!
Happy building!















Comments