Particulates kill, build your sensor now!
(Updated: )
One metric still missing from my home sensors is particulates, which are a known health hazard. Although I (secretly) already had a board running for a while, I wasn’t satisfied enough to share it. Today, it’s ready for sharing, including screaming headline for a change ;).
This board builds on my previous (smaller) design without particulate sensor, and also sports a custom-designed PCB (which you can order here!).
Starting with the end, it looks like:
Introduction
Building on my previous sensor projects, I’m now adding a Nova Fitness SDS011 PM2.5/PM10 fine dust sensor, widely used & recommended by e.g. RIVM, Luftdaten, and EU VAQUUMS.
For basic/context/intro, please read my previous guides here and here.
Bill of materials
- Special-built BigSensorThing PCB -- 9 euros with same-day shipping!
- ESP8266 WeMos D1 mini (do not get the pro! it’s too big)
- Nova Fitness SDS011 fine dust sensor
- Winsen MH-Z19B CO2 sensor
- Optional: 1.3" 128x64 OLED or 1.5" 128x128 OLED (I use a 1.5" here)
- BME280 module with level converter - N.B. ensure you have a BME280 module that accepts 5V to use my print!
- DS18B20 temperature sensor
- Male pin headers straight (for modules)
- Male pin headers 90 degrees (for SDS011 & optionally DS18B20)
- Optional: Dupont female-female jumper wires (to increase distance of DS18B20)
- M3 screws >20mm thread or 2x 10mm M3 afstandsbusjes -- you need at least 15mm clearance between the PCB and the SDS011
- Optional: get a hose to sample air from a distance
Some caveats/considerations:
- Get a flexible/small USB micro cable. The Wemos/Lolin D1 Mini Pro is a bit too long to comfortably fit a USB cable at the bottom without extruding. I actually built mine with the Pro before I figured this out, and it sort of stands because I had a very flexible USB cable. Even with the regular (smaller) Wemos D1 mini, a short/flexible USB micro plug helps.
- Since I power everything on 5V and the BME280 only accepts 1.7-3.6V as input voltage, ensure you get a BME280 module with voltage regulator, e.g. the one linked above. It should explicitly note that input voltage can be either 3.3V or 5V.
Bill of process
Software integration
Install esphome as documented here.
Hardware integration
Solder male header pins onto PCB for (1) D1 mini, (2) MH-Z19B, (3) OLED, (4) BME280, (5) DS18B20 and (6) SDS011 connector (use 90 deg here!)
N.B. For the MH-Z19B you can save a few headers by only connecting the necessary VCC, GND, RX and TX pins. (You could also only solder used pins for the D1 mini but I chose to connect all so I can optionally connect additional stuff via jumper wires).
N.B. 2 you can connect the DS18B20 on the front or on the back
Solder on modules.
Sandwitch PCB & SDS011, and wrap serial cable around / through the sandwich. Optionally (this design), connect the DS18B20 using double female Dupont wires to reduce/prevent self-heating.
This design directly powers the ESP8266 board, so no need for (micro) USB port as in previous designs.
Software configuration
Again similar to previous projects, with modification to add SDS011. Some notes:
- Ensure you reduce OLED refresh rate to e.g. 30s
- I only got stable operation by disabling UART logger.
- Optionally use deep sleep loop, losing EMA
- I filter some sensors as exponential moving average (EMA) mode as well to easily get a average of the last ~12hrs. Somehow I can’t get all sensors to run like this, perhaps I maxed out the ESP8266.
esphome:
name: esp_bigsensorthing
platform: ESP8266
board: d1_mini_pro # Change to d1_mini if you don't have a pro
wifi:
ssid: "your wifi SSID"
password: "your wifi password"
#fast_connect: True # Required to connect to hidden SSIDs and only with one network
domain: ".lan"
# reboot_timeout: 0s # Do not reboot if no wifi (can be useful for offline use)
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Esp Living Fallback Hotspot"
password: "your fallback password"
logger:
# Disable because somehow this conflicts with double serial ports of SDS011 and MH-Z19B
# Disable UART Logging to fix MHZ-19B preamble issue when using hardware UART, see https://github.com/esphome/issues/issues/488
# N.B. disabling logger crashes my Mac because of buggy CH34x driver, so I disable it as last setting.
baud_rate: 0
# level: VERBOSE
# Enable Home Assistant API for logging over wifi.
# Disable reboot loop by setting reboot_timeout to 0s!
# See warning on https://esphome.io/components/mqtt.html
api:
reboot_timeout: 0s
# Disable webserver, since we push data over mqtt. Can be useful for
# diagnostics, but takes up quite some memory
# web_server:
# port: 80
# Optionally use deep sleep loop to reduce power consumption/self-heating.
# deep_sleep:
# run_duration: 30s
# sleep_duration: 150s
# Allow OTA updates
ota:
mqtt:
# For mobile = WAN : use FQDN, for local (IoT network - no WAN), use home IP.
broker: "192.168.0.1"
#broker: home.yourhostname.org
port: 1883
username: "esp_board_client"
password: "AQFCg72z5MqFihspHGbkqOj9"
uart:
- id: myuart0 # For SDS011 pm2.5/pm10
rx_pin: GPIO14 # = D5
tx_pin: GPIO12 # = D6
baud_rate: 9600
- id: myuart1 # For MH-Z19B CO2
rx_pin: GPIO13 # = D7
tx_pin: GPIO15 # = D8
baud_rate: 9600
# For BME280 & OLED
i2c:
sda: GPIO4 # = D2 = 4
scl: GPIO5 # = D1 = 5
scan: False
frequency: 100kHz # Trying to get more stable OLED value display
# For Dallas temp sensors connected to pin GPIO0 = D3, enable internal pull-up
# resistor
dallas:
id: ds18b20_temp_sensor
pin:
number: GPIO0 # D3 = 0
# inverted: True
mode: INPUT_PULLUP
update_interval: 60s # if changed also update EMA alpha
sensor:
- platform: wifi_signal
name: "WiFi Signal"
update_interval: 60s
id: mywifi
- platform: uptime
name: Uptime
update_interval: 60s
id: myuptime
- platform: sds011
pm_2_5:
state_topic: influx/environv3/quantity/pm25/source/sds011/board/esp_bigsensorthing/location/home/room/living/value/state
name: "SDS011 PM2.5"
id: sds011_pm25
pm_10_0:
state_topic: influx/environv3/quantity/pm10/source/sds011/board/esp_bigsensorthing/location/home/room/living/value/state
name: "SDS011 PM10"
id: sds011_pm10
update_interval: 10min # factory default 0min, set to 10min to save limited 8000h laser lifetime. Also reduces fan noise
id: mysds011
uart_id: myuart0
- platform: dallas
index: 0
name: "DS18B20 Temperature"
state_topic: influx/environv3/quantity/T/source/dallas1/board/esp_bigsensorthing/location/home/room/living/value/state
id: ds18b20_temp
- platform: mhz19
co2:
name: "MH-Z19 CO2"
state_topic: influx/environv3/quantity/CO2/source/mhz19b/board/esp_bigsensorthing/location/home/room/living/value/state
id: mhz_19_co2
temperature:
name: "MH-Z19 Temperature"
state_topic: influx/environv3/quantity/T/source/mhz19b/board/esp_bigsensorthing/location/home/room/living/value/state
id: mhz_19_temp
update_interval: 60s
uart_id: myuart1
- platform: mhz19
co2:
name: "MH-Z19 CO2"
id: mhz_19_co2_ema
filters:
- exponential_moving_average:
alpha: 0.0028 # We want ~12 hour averaging, at 60s update, that's 12*3600/60 = 720points, thus alpha should be 2/1440 ~ 0.0028
send_every: 1
update_interval: 60s # if changed also update alpha
uart_id: myuart1
- platform: bme280
temperature:
name: "BME280 Temperature"
oversampling: 1x
id: bme_280_temp
state_topic: influx/environv3/quantity/T/source/bme280/board/esp_bigsensorthing/location/home/room/living/value/state
pressure:
name: "BME280 Pressure"
id: bme_280_press
state_topic: influx/environv3/quantity/P/source/bme280/board/esp_bigsensorthing/location/home/room/living/value/state
humidity:
name: "BME280 Humidity"
id: bme_280_rh
state_topic: influx/environv3/quantity/RH/source/bme280/board/esp_bigsensorthing/location/home/room/living/value/state
address: 0x76
update_interval: 60s
- platform: bme280
humidity:
name: "BME280 Humidity"
id: bme_280_rh_ema
filters:
- exponential_moving_average:
alpha: 0.0028 # We want ~12 hour averaging, at 60s update, that's 12*3600/60 = 720points, thus alpha should be 2/1440 ~ 0.0028
send_every: 1
address: 0x76
update_interval: 60s # if you change this don't forget to update alpha
font:
- file: "slkscr.ttf"
id: my_font1
size: 8
- file: "Arial.ttf"
id: my_font3
size: 16
# # I want to use ø, drop unused chars in return
glyphs:
['ø', '~', '.', '%', '(', ')', '+', '-', '_', ':', '°', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', ' ', '/']
display:
- platform: ssd1327_i2c
model: "SSD1327 128x128"
update_interval: 30s # Don't forget to set this to prevent esphome crash due to default 5s
# reset_pin: D0
address: 0x3C
rotation: 180°
# Small font, without EMA
# lambda: |-
# it.printf(0, 0, id(my_font1), "CO2 (PPM): %d", int(id(mhz_19_co2).state));
# it.printf(0, 10, id(my_font1), "T1 (C): %.1f", id(ds18b20_temp).state);
# it.printf(0, 20, id(my_font1), "T2 (C): %.1f", id(bme_280_temp).state);
# it.printf(0, 30, id(my_font1), "RH (%%): %.1f", id(bme_280_rh).state);
# it.printf(0, 40, id(my_font1), "P (HPa): %d", int(id(bme_280_press).state));
# it.printf(0, 50, id(my_font1), "PM (2.5): %5d", int(id(sds011_pm25).state));
# it.printf(0, 60, id(my_font1), "PM (10): %5d", int(id(sds011_pm10).state));
# Bigger font, mixed EMA
lambda: |-
it.printf(0, 0, id(my_font1), "CO2");
it.printf(0, 8, id(my_font1), "PPM");
it.printf(24, 0, id(my_font3), "%d (~%d)", int(id(mhz_19_co2).state), int(id(mhz_19_co2_ema).state));
it.printf(0, 16, id(my_font1), "T1");
it.printf(0, 24, id(my_font1), "C");
it.printf(24, 16, id(my_font3), "%.1f", id(ds18b20_temp).state);
it.printf(0, 32, id(my_font1), "T2");
it.printf(0, 40, id(my_font1), "C");
it.printf(24, 32, id(my_font3), "%.1f", id(bme_280_temp).state);
it.printf(0, 48, id(my_font1), "RH");
it.printf(0, 56, id(my_font1), "%%");
it.printf(24, 48, id(my_font3), "%.1f (~%.1f)", (id(bme_280_rh).state), (id(bme_280_rh_ema).state));
it.printf(0, 64, id(my_font1), "P");
it.printf(0, 72, id(my_font1), "HPa");
it.printf(24, 64, id(my_font3), "%d", int(id(bme_280_press).state));
it.printf(0, 80, id(my_font1), "PM");
it.printf(0, 88, id(my_font1), "2.5");
it.printf(24, 80, id(my_font3), "%.1f", id(sds011_pm25).state);
it.printf(0, 96, id(my_font1), "PM");
it.printf(0, 104, id(my_font1), "10");
it.printf(24, 96, id(my_font3), "%.1f", id(sds011_pm10).state);