IoT Air Purifier
Last november I bought an air purifier on amazon. The selling point was the hepa filter and the low price. This thing is “ok”, per se: it filters air, it has got a button and some leds.
Then comes the noise… there’s no way you can have this thing screaming in the bedroom all the night. And the published 32db are plain bullshit.
Before
As you can guess, there’s a standard fan inside.
Inside
It’s a standard 80mm 5V DC fan, without PWM, without tachymetric return, DC only. There’s also a connector board, and something hidden below.
Below the fan there’s a PCB, which manages the four LEDs, the button and controls the FAN.
My whishlist
These are the modifications planned:
- swap the noisy fan for a much quieter Noctua
- install a D1-mini to control the fan, the button and the LEDs
- if possible, no external modification
PCB modding
Since I don’t want to resolder the button and invent something for the LEDs, I tried to use the existing PCB.
I used a multimeter to map the three pins on the top, which are (from left to right): FAN_OUT, GND, 5VDC.
I then mapped the pins on the IC on the bottom, and then I removed it.
signal | PIN | PIN | signal |
---|---|---|---|
FAN OUT | 5 | 4 | Button |
LED 2 | 6 | 3 | LED 1 |
LED 3 | 7 | 2 | LED 4 |
GND | 8 | 1 | Vcc |
I soldered some wires for the 4 LEDs and the button. Power and ground are taken from the top 3 pins. This soldering is nothing I’m proud of, but believe me, the hot glue I poured onto the PCB for “isolation” is something I don’t want to show.
The new fan
This fan (Noctua NF-A8 PWM) is amazing. At “normal” speed, it is almost silent. And it can be controlled by PWM. I needed a PWM fan because I can power it using the 5V from the power supply and control its speed via a signal from the D1-mini. The alternative was an external amplifier to drive the fan in DC. I like the PWM solution more.
So I chopped off the connector from the Noctua and connected everything to a D1-mini.
The controller
We’ll see the software part later on.
I soldered the wires on the D1-mini and fitted the D1-mini in the place where the connector board was. Then I encountered a small problem: the D1-mini is a bit bigger than the previous board and 2 plastic bezels were blocking the cover so I couldn’t close it.
Say goodbye to these plastic bezels.
The software
The best way to DIY something like this is ESPHome. I use it in my Home Assistant, so it’s easier to let them talk.
This is the source:
1substitutions:
2 display_name: my_purifier
3 use_address: !secret ip_my_purifier
4 static_ip: !secret ip_my_purifier
5
6esphome:
7 platform: ESP8266
8 name: ${display_name}
9 board: d1_mini
10 on_boot:
11 priority: -10
12 then:
13 - delay: 2s
14 - fan.turn_on:
15 id: myfan
16 speed: LOW
17
18wifi:
19 use_address: ${use_address}
20 ssid: !secret iot_wifi_ssid
21 password: !secret iot_wifi_password
22 fast_connect: true
23 power_save_mode: none
24 reboot_timeout: 10min
25 manual_ip:
26 static_ip: ${static_ip}
27 gateway: !secret iot_network_gateway
28 subnet: !secret iot_network_subnet
29 dns2: !secret iot_network_dns2
30 dns1: !secret iot_network_dns1
31
32web_server:
33 port: 80
34
35logger:
36
37api:
38 reboot_timeout: 0s
39
40ota:
41 password: !secret ota_password
42
43sensor:
44 - platform: wifi_signal
45 name: ${display_name} WiFi RSSI
46 update_interval: 60s
47
48switch:
49 - platform: restart
50 name: ${display_name} Restart
51 id: restart_switch
52
53time:
54 - platform: sntp
55 id: mytime
56 servers:
57 - !secret iot_ntp_server
58 on_time:
59 - seconds: 0
60 minutes: 0
61 hours: 4
62 days_of_week: MON-SUN
63 then:
64 - switch.toggle: restart_switch
65
66mqtt:
67 broker: !secret mqtt_broker
68 username: !secret mqtt_username
69 password: !secret mqtt_password
70 discovery: True
71
72# PIN ASSIGNMENT
73#
74# D0 fan tachy
75# D1 led1
76# D2 led2
77# D3 led3
78# D4 status led
79# D5 led4
80# D6 fan pwm
81# D7 button
82# D8 - spare
83
84status_led:
85 pin:
86 number: D4
87 inverted: True
88
89fan:
90 - platform: speed
91 output: pwm_fan_output
92 name: ${display_name} fan
93 id: myfan
94 speed:
95 low: 0.25
96 medium: 0.5
97 high: 0.9
98
99# Noctua NF-A8 5V PWM, tech specs:
100# https://noctua.at/en/nf-a8-5v-pwm/specification
101# at 100% the fan runs at 2200 RPM, +/- 10%
102# the minimum speed is 450 RPM, +/- 20%, we use a 540rpm, which is around 25%
103
104# FAN wire colors:
105# - blue: PWM command
106# - green: fan tachy return
107# - yellow: 5V
108# - black: GND
109
110light:
111 - platform: monochromatic
112 output: pwm_led1_output
113 name: ${display_name} LED1
114 id: led_light_1
115 effects:
116 - lambda:
117 name: Breathing SLOW
118 update_interval: 10s
119 lambda: |-
120 static int state = 0;
121 static int color = 1;
122 auto call = id(led_light_1).turn_on();
123 call.set_transition_length(9300);
124 if (state == 0) {
125 call.set_brightness(1.0);
126 } else if (state == 1) {
127 call.set_brightness(0.01);
128 }
129 call.perform();
130 state ++;
131 if (state == 2){
132 state = 0;
133 }
134 - lambda:
135 name: Breathing MID
136 update_interval: 5s
137 lambda: |-
138 static int state = 0;
139 static int color = 1;
140 auto call = id(led_light_1).turn_on();
141 call.set_transition_length(4500);
142 if (state == 0) {
143 call.set_brightness(1.0);
144 } else if (state == 1) {
145 call.set_brightness(0.01);
146 }
147 call.perform();
148 state ++;
149 if (state == 2){
150 state = 0;
151 }
152 - lambda:
153 name: Breathing FAST
154 update_interval: 2s
155 lambda: |-
156 static int state = 0;
157 static int color = 1;
158 auto call = id(led_light_1).turn_on();
159 call.set_transition_length(1800);
160 if (state == 0) {
161 call.set_brightness(1.0);
162 } else if (state == 1) {
163 call.set_brightness(0.01);
164 }
165 call.perform();
166 state ++;
167 if (state == 2){
168 state = 0;
169 }
170
171 - platform: monochromatic
172 output: pwm_led2_output
173 name: ${display_name} LED2
174 id: led_light_2
175 effects:
176 - lambda:
177 name: Breathing SLOW
178 update_interval: 10s
179 lambda: |-
180 static int state = 0;
181 static int color = 1;
182 auto call = id(led_light_2).turn_on();
183 call.set_transition_length(9300);
184 if (state == 0) {
185 call.set_brightness(1.0);
186 } else if (state == 1) {
187 call.set_brightness(0.01);
188 }
189 call.perform();
190 state ++;
191 if (state == 2){
192 state = 0;
193 }
194 - lambda:
195 name: Breathing MID
196 update_interval: 5s
197 lambda: |-
198 static int state = 0;
199 static int color = 1;
200 auto call = id(led_light_2).turn_on();
201 call.set_transition_length(4500);
202 if (state == 0) {
203 call.set_brightness(1.0);
204 } else if (state == 1) {
205 call.set_brightness(0.01);
206 }
207 call.perform();
208 state ++;
209 if (state == 2){
210 state = 0;
211 }
212 - lambda:
213 name: Breathing FAST
214 update_interval: 2s
215 lambda: |-
216 static int state = 0;
217 static int color = 1;
218 auto call = id(led_light_2).turn_on();
219 call.set_transition_length(1800);
220 if (state == 0) {
221 call.set_brightness(1.0);
222 } else if (state == 1) {
223 call.set_brightness(0.01);
224 }
225 call.perform();
226 state ++;
227 if (state == 2){
228 state = 0;
229 }
230
231 - platform: monochromatic
232 output: pwm_led3_output
233 name: ${display_name} LED3
234 id: led_light_3
235 effects:
236 - lambda:
237 name: Breathing SLOW
238 update_interval: 10s
239 lambda: |-
240 static int state = 0;
241 static int color = 1;
242 auto call = id(led_light_3).turn_on();
243 call.set_transition_length(9300);
244 if (state == 0) {
245 call.set_brightness(1.0);
246 } else if (state == 1) {
247 call.set_brightness(0.01);
248 }
249 call.perform();
250 state ++;
251 if (state == 2){
252 state = 0;
253 }
254 - lambda:
255 name: Breathing MID
256 update_interval: 5s
257 lambda: |-
258 static int state = 0;
259 static int color = 1;
260 auto call = id(led_light_3).turn_on();
261 call.set_transition_length(4500);
262 if (state == 0) {
263 call.set_brightness(1.0);
264 } else if (state == 1) {
265 call.set_brightness(0.01);
266 }
267 call.perform();
268 state ++;
269 if (state == 2){
270 state = 0;
271 }
272 - lambda:
273 name: Breathing FAST
274 update_interval: 2s
275 lambda: |-
276 static int state = 0;
277 static int color = 1;
278 auto call = id(led_light_3).turn_on();
279 call.set_transition_length(1800);
280 if (state == 0) {
281 call.set_brightness(1.0);
282 } else if (state == 1) {
283 call.set_brightness(0.01);
284 }
285 call.perform();
286 state ++;
287 if (state == 2){
288 state = 0;
289 }
290
291 - platform: monochromatic
292 output: pwm_led4_output
293 name: ${display_name} LED4
294 id: led_light_4
295 effects:
296 - lambda:
297 name: Breathing SLOW
298 update_interval: 10s
299 lambda: |-
300 static int state = 0;
301 static int color = 1;
302 auto call = id(led_light_4).turn_on();
303 call.set_transition_length(9300);
304 if (state == 0) {
305 call.set_brightness(1.0);
306 } else if (state == 1) {
307 call.set_brightness(0.01);
308 }
309 call.perform();
310 state ++;
311 if (state == 2){
312 state = 0;
313 }
314 - lambda:
315 name: Breathing MID
316 update_interval: 5s
317 lambda: |-
318 static int state = 0;
319 static int color = 1;
320 auto call = id(led_light_4).turn_on();
321 call.set_transition_length(4500);
322 if (state == 0) {
323 call.set_brightness(1.0);
324 } else if (state == 1) {
325 call.set_brightness(0.01);
326 }
327 call.perform();
328 state ++;
329 if (state == 2){
330 state = 0;
331 }
332 - lambda:
333 name: Breathing FAST
334 update_interval: 2s
335 lambda: |-
336 static int state = 0;
337 static int color = 1;
338 auto call = id(led_light_4).turn_on();
339 call.set_transition_length(1800);
340 if (state == 0) {
341 call.set_brightness(1.0);
342 } else if (state == 1) {
343 call.set_brightness(0.01);
344 }
345 call.perform();
346 state ++;
347 if (state == 2){
348 state = 0;
349 }
350output:
351 - platform: esp8266_pwm
352 pin: D6
353 frequency: 2200 Hz
354 id: pwm_fan_output
355
356 - platform: esp8266_pwm
357 pin: D1
358 inverted: true
359 frequency: 1000 Hz
360 id: pwm_led1_output
361 - platform: esp8266_pwm
362 pin: D2
363 inverted: true
364 frequency: 1000 Hz
365 id: pwm_led2_output
366 - platform: esp8266_pwm
367 pin: D3
368 inverted: true
369 frequency: 1000 Hz
370 id: pwm_led3_output
371 - platform: esp8266_pwm
372 pin: D5
373 inverted: true
374 frequency: 1000 Hz
375 id: pwm_led4_output
376
377binary_sensor:
378 - platform: gpio
379 pin:
380 number: D7
381 mode: INPUT_PULLUP
382 name: ${display_name} button
383 filters:
384 - invert:
385 - delayed_on: 10ms
386
387 - platform: status
388 name: ${display_name} Status
I use MQTT in my setup, but it’s not the common choice. This would be using the ESPhome APIs which integrates seamlessly into Home Assistant.
If you want to use the above yaml, you have to provide your settings in place of the “secrets”.
I usually put in all my ESPhome devices:
- a sensor for the wifi signal
- a status binary sensor to check if the device is connected from Home Assistant
- a weekly reboot
- a remote restart command
- a “status led” function, which I use on the D1-mini LED
On device start, the fan is switched on at low speed.
The button switches on and off the fan, but doesn’t control the speed. I didn’t implement this because I’ll probably never use the button anyway.
I still don’t know what I want the 4 LEDs to do. But I added a PWM control for all of them, combined with some “breathing” effect, in 3 different speeds. I can use these LEDs as light components in Home Assistant, so I can precisely dim the LEDs at specified brightness, or I can use these “breathing” effects.
The only interesting automation I did in Home Assistant is the speed change of the fan, according to the status of my room light: when I enter the room and I switch on the light, the speed is reduced to LOW; when I exit and switch off the light, the speed is changed to MID.
I also added a telegram warning if the device is disconnected for more than 5 minutes.
I connected but didn’t use the fan return signal. Maybe in the future I’ll add an alarm if the fan is stuck.
Costs
Somebody asked for the cost of this project; here’s the part list:
- The original purifier - it was in promo at € 19,66
- D1 mini - I bought a pack of three, at € 5,33 cad.
- Noctua fan - € 15,90
So the grand total is: € 40,89
Coming up next
I bought an air quality sensor.