Air quality measurement sensor with ESPhome

Got my esp32 and it’s working fine as well.

1 Like

I’ve had an issue with the ESP8266 which stumped me for a while.

If you’re having trouble getting this to work on a generic ESP8266:

  • Note that GPIO0 (pin D3) might not work as a GPIO switch

I changed to GPIO14 (D5) and things started working.

You can verify this by running a volt meter on the pin. If the GPIO switch is turned on, that pin should read 3.3v.

Hope this helps those who are having trouble with this.

I’m fairly new to the DIY part of sensors, and for a while now I’ve been looking for something that would provide the exact sensors as provided on this tutorial, but also I was hoping to integrate a CO²/VOC sensors as well.
Something like this one that I’ve found:

DIY Air Quality Monitor - PM2.5, CO2, VOC, Ozone, Temp & Hum Arduino Meter (howtomechatronics.com)

but…as far as my research went, this solution cannot report back to HA.
So is there any chance that we could take this tutorial as base and add another sensors?

Got the same noise, someone got a solution or is the sensor just not good?

Wow…I feel like I just have to add, your build is tiny, neat and to the point. I have a very similar scope and thanks to your full article made it come together in like an hour.

I am still fussing over graph look and feel, but this is a really nice build.

Thanks from WV/USA.

@pbrink THANKYOU so much for writing this up and sharing.
I have a working Air Quality sensor, feeding reliably into HA, in an afternoon :slight_smile: Thankyou.

(And thanks to all the ESPhome devs particularly those who worked on the PMSX003 platform)

There appears to be a much simpler way to sleep the PMS device now.
(Step 5. Extending the life span of the PMS5003 sensor** in the blog article.)

I noticed in the ESPhome PMSX003 platform documentation:

Sensor Longevity

The laser diode inside the PMSX003 has a lifetime of about 8000 hours, nearly one year.

If you wish to use the optional update_interval ensure you have a tx_pin set in the UART configuration and connected to the RECEIVE/RX pin (may also be called the TX pin, depending on the model) of the PMS. Setting update_interval to 120 seconds or higher may help extend the life span of the sensor.

  • update_interval (Optional): Amount of time to wait between generating measurements. If this is longer than 30 seconds, and if tx_pin is set in the UART configuration, the fan will be spun down between measurements. Default to 0s (forward data as it’s coming in from the sensor).

I read that as:

  • The ESP32’s RX only needs to be connected to the sensors TX to get data
  • If the update_interval is set to more than the (default and minimum) 30 seconds then the platform will set the ESP32’s TX low after a reading
  • By connecting the ESP32’s TX to the SET (pin3) on the sensor it will automatically shutdown between readings

This is what I have done and the fan stops between readings (with no extra code or faff)
I’m guessing this was put into the platform after your original blog article.
(Please come back to me if this doesn’t make sense)

More info on extending the life.

The ESPHome pmsx003 module was updated ~May2022 to include a software solution to stop the fan between readings. Its done by sending a command over the UART to start and stop the fan as required. So connecting PIN3 on the PMS to a GPIO and have a loop to turn it on and off is not required.

If you use this, then 30 seconds before a reading is taken the fan starts up, spins for 30 seconds and after the reading it taken the fan stops and stays stopped until 30 seconds before the next reading.

So if you want data every 60 seconds, stopping the fan for 30 seconds ~doubles the fan’s/ device’s life
If you want readings every two minutes then you get 4 times the life (approx) etc

This is documented here:
(Jan16-May10, 2022) https://github.com/esphome/esphome/pull/3053
and more background: https://github.com/esphome/feature-requests/issues/2033 & the links in this request

This is the full code I’m using for Air Quality Monitoring with the PMS sensor:

#ESPHome Particulate (+Temperature and Humdity) Sensor
#For Home Assistant 
#Using PMS5003T sensor and Lilygo Mini32 V1.5 (ESP32-WROVER-B Dual Core Wi-Fi & Bluetooth Module)

# - Measures Particulates, temperature and humdity every 150seconds (2.5 minutes)
# - Sends data back to home assistant
# - Shutdowns PMS Sensors laser/ fan beteween readings so maximise the sensors limited life
# - Flashes onboard LED when a measurement is taken
# - Flashes onboard LED every 10 seconds as evidence ESP32 is alive

#Hardware
#PMS5003T
#Pin 1 5v (VCC+)
#    2 GND from ESP32
#    3 SET Not connected, and not required see note below
#    4 RX connected to ESP32 TX 
#    5 TX connected to ESP32 RX
#    6,7,8 not connected

#NOTE: FAN spin
#  The fan is spun down by the ESP32's pmsx003 module using a software/UART command rather
#  than using PIN 3
#  This is documented here:
#    (Jan16-May10, 2022) https://github.com/esphome/esphome/pull/3053 
#    and more background: https://github.com/esphome/feature-requests/issues/2033 & the links is request

#Original & Credit Pieter Brinkman (And ESPHome and pmsx003 module developers [thanyou guys])
#https://www.pieterbrinkman.com/2021/02/03/build-a-cheap-air-quality-meter-using-esphome-home-assistant-and-a-particulate-matter-sensor/

#Lilygo:
#  Requires board: wemos_d1_mini32
#  https://github.com/LilyGO/TTGO-T7-Demo/blob/ef0f6b1589f7aec405114c6bda6b5a734d38150b/images/T7V1.5.jpg
#  https://www.lilygo.cc/products/t7-mini32-v1-5

#Notes 
#  If using a different varient of the pmsx003 sensor platform comment out the temperature and humdity etc as required
#  As of October2023 update_interval of greater than 30 seconds will signal PMS to shut down if ESP32 TX connected to PMS RX

substitutions:
  devicename: AQM
  friendly_name: AirQualityMonitor
  device_description: VOC temperature and humidity monitor

  #Time between measurement samples
  measurement_interval: "250s" #250s = 2.5minutes

  #Location / Prefix (prefix for sensor name, useful if using multiple sensors around the home)
  location: office

esphome:
  name: ${devicename}
  comment: ${device_description}
  on_boot:
    then:
      - delay: 1s
      - script.execute: BootFlash

esp32:
  board: wemos_d1_mini32
  framework:
    type: arduino

# Enable logging
logger:
  baud_rate: 0 #disable as used by UART

# Enable Home Assistant API
api:
  encryption:
    key: "redacted"

ota:
  password: "redacted

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Fallback Hotspot"
    password: "redacted"

captive_portal:

###############################################################################################
#HARDWARE and SENSOR CONFIGURATION
#

# Define onboard LED
output:
  - platform: gpio
    pin: GPIO19
    id: OnboardLED

#Air Quality Sensor
uart:
  tx_pin: GPIO001 #Used by ESPHome PMS Platform to turn off fan/laser
  rx_pin: GPIO003
  baud_rate: 9600

sensor:
  - platform: pmsx003
    type: PMS5003T
    pm_1_0:
      name: "${location}_PMS_Particulate<1.0µm"
    pm_2_5:
      name: "${location}_PMS_Particulate<2.5µm"
    pm_10_0:
      name: "${location}_PMS_Particulate<10.0µm"
      #Flash LED when new sample measured
      on_value:
        then:
          - script.execute: FlashSample
    temperature:
      name: "${location}_PMSTemperature"
      accuracy_decimals: 2
    humidity:
      name: "${location}_PMSHumidity"
      accuracy_decimals: 0
    update_interval: ${measurement_interval}

######################################################################################
#CONTROL
#
interval:
  #Alive blink
  - interval: 10s
    then:
      - if:
          condition:
            script.is_running: FlashSample
          then:
            #do nothing
          else:
            - script.execute: FlashAlive


####################################################################################
#SCRIPTS
#
script:
  - id: BootFlash
    mode: single
    then:
      - repeat:
          count: 4
          then:
            - output.turn_on: OnboardLED
            - delay: 500ms
            - output.turn_off: OnboardLED
            - delay: 500ms

  - id: FlashAlive
    mode: single
    then:
      - output.turn_on: OnboardLED
      - delay: 500ms
      - output.turn_off: OnboardLED

  - id: FlashSample
    mode: single
    then:
      - repeat:
          count: 7
          then:
            - script.execute: FlashDoubleQuick
            - delay: 300ms

  - id: FlashDoubleQuick
    mode: single
    then:
      - output.turn_on: OnboardLED
      - delay: 30ms
      - output.turn_off: OnboardLED
      - delay: 1ms
      - output.turn_on: OnboardLED
      - delay: 30ms
      - output.turn_off: OnboardLED
1 Like

Thank you for this @pbrink, I have mine up and running! Quick question on the math though… if the PM10 sensor truly measures anything less than 10um, does that mean we need to add math to screen out the PM1 and PM2.5 values to get a true PM10 reading? Eg. PM10 = PM10 - PM2.5 ? And for PM2.5, PM2.5 = PM2.5 - PM1. This is my assumption due to the labeling of each sensor with a “less than” sign. Or does the PM2.5 sensor actually have a lower range as well…eg. “anything smaller than 2.5um but larger than 1.0um?

This article seems to indicate that you would have to do some math…and the fact that these sensors include numbers for ALL smaller categories is why the PM10 number is always higher than the PM2.5 number and the PM2.5 number is always higher than the PM1. If this is true, shouldn’t we be subtracting in the sensors or the Lovelace code to get valid numbers? Otherwise it seems like the PM2.5 and PM10 numbers would be artificially high.

The specs of the sensor under “Particle measurement range” tell you they all measure different ranges i.e. 0.3-1, 1-2.5 and 2.5-10um

Hello and happy new year…
I am using part of your code and setup and just added a sdc30 for c02 on top and works flawless.
The only question i have is about the 2 min delay measurement.
I get constant logs for the particulates and not just every 2 min.
How can i check if my pms sensor is obeying the intervals?
Here my code:

sensor:
  # Uptime sensor.
  - platform: uptime
    name: ${upper_devicename} Uptime
    filters:
      - lambda: return x / 60.0;
    unit_of_measurement: minutes
    
  # WiFi Signal sensor.
  - platform: wifi_signal
    name: ${upper_devicename} WiFi Signal
    update_interval: 60s   
     
  - platform: pmsx003
    type: PMSX003
    uart_id: uart_1
    pm_1_0:
      id: pm10
      name: "Fein Partikel <1.0µm: "
    pm_2_5:
      id: pm25
      name: "Fein Partikel <2.5µm: "
    pm_10_0:
      id: pm100
      name: "Fein Partikel <10.0µm: "

  - platform: scd30
    co2:
      id: co2
      name: "CO2"
      accuracy_decimals: 1
    temperature:
      id: temp
      name: "Temperature"
      accuracy_decimals: 2
    humidity:
      id: humidity
      name: "Humidity"
      accuracy_decimals: 1
    temperature_offset: 1.5 °C
    address: 0x61
    i2c_id: bus_a
    update_interval: 15s

i2c:
   - id: bus_a
     sda: D1
     scl: D2
     scan: true
#   - id: bus_b
#     sda: D7
#     scl: D8
#     scan: true

uart:
  - rx_pin: D6
    tx_pin: D5
    baud_rate: 9600
    id: uart_1

#font:
#  - file: "monofont.ttf"
#    id: opensans
#    size: 10
#
#display:
#  - platform: ssd1306_i2c
#    address: 0x3C
#    i2c_id: bus_a
#    id: oled
#    model: "SSD1306 64x48"
#    pages:
#      - id: page1
#        lambda: |-
#          it.printf(0, 0, id(opensans), "CO2: %.0fppm", id(co2).state);
#          it.printf(0, 10, id(opensans), "PM10 : %.0f", id(pm10).state);
#          it.printf(0, 20, id(opensans), "PM25 : %.0f", id(pm25).state);
#          it.printf(0, 30, id(opensans), "PM100: %.0f", id(pm100).state);
#      - id: page2
#        lambda: |-
#          it.printf(0, 20, id(opensans), "Hmdty: %.0f", id(humidity).state);
#          it.printf(0, 30, id(opensans), "Temp: %.0fC", id(temp).state);


switch:
  - platform: restart
    name: "${upper_devicename} restart"
  - platform: gpio
    pin: 
      number: D3
    id: pms_set
    name: "Start measuring"


interval:
 # - interval: 5s
 #   then:
 #     - display.page.show_next: oled
 #     - component.update: oled
  - interval: 120s
    then:
      - switch.turn_on: pms_set
      - delay: 20s
      - switch.turn_off: pms_set

number:
  - platform: template
    name: "CO2 calibration value"
    optimistic: true
    min_value: 400
    max_value: 450
    step: 1
    id: co2_cal
    icon: "mdi:molecule-co2"
    entity_category: "config"

button:
  - platform: template
    name: "SCD30 Force manual calibration"
    entity_category: "config"
    on_press:
      then:
        - scd30.force_recalibration_with_reference:
            value: !lambda 'return id(co2_cal).state;'

Thank you for sharing your code.

This is a very elegant solution and I have implemented it instead of the original approach with SET pin.

I am however curious on where you set the 30s for the fan to start moving before measurement.

In the original approach the measurement is enabled for 20s and I can see in the log how the values are changing during the 20s. I assume you achieve the same by having the fan turn on 30s before measurement, but can you please confirm…