Indoor Air Quality Sensor Component

Dude, @fahr that is awesome! I didn’t even think about an AQI value, until your post. Just yesterday I got my first pm2.5 sensor from AliExpress (pms7003) and got it working on ESPHome because they recently integrated the pmsx003. I took your equation and matched it up with the one on Wikipedia’s AQI page and adapted it for use in an ESPHome template sensor and template text sensor for the color:

uart:
  rx_pin: 16
  baud_rate: 9600

globals:
  - id: c_low
    type: float
  - id: c_high
    type: float
  - id: i_low
    type: float
  - id: i_high
    type: float
    
sensor:
  - platform: pmsx003
    type: PMSX003
    pm_2_5:
      name: "Particulate Matter <2.5µm Concentration"
      id: pm25
      accuracy_decimals: 1
      filters:
        - sliding_window_moving_average:
            window_size: 60
            send_every: 31
            send_first_at: 11

  # https://en.wikipedia.org/wiki/Air_quality_index
  - platform: template
    name: Air Quality Index
    unit_of_measurement: AQI
    icon: mdi:pine-tree-fire
    accuracy_decimals: 0
    lambda: >
      if (id(pm25).state > 500.4) {
        return NAN;
      } else if (id(pm25).state >= 350.5) {
        id(i_high) = 500.0;
        id(i_low)  = 401.0;
        id(c_high) = 500.0;
        id(c_low)  = 350.5;
      } else if (id(pm25).state >= 250.5) {
        id(i_high) = 400.0;
        id(i_low)  = 301.0;
        id(c_high) = 350.4;
        id(c_low)  = 250.5;
        id(aqi_color).publish_state("Maroon");
      } else if (id(pm25).state >= 150.5) {
        id(i_high) = 300.0;
        id(i_low)  = 201.0;
        id(c_high) = 250.4;
        id(c_low)  = 150.5;
        id(aqi_color).publish_state("Purple");
      } else if (id(pm25).state >= 55.5) {
        id(i_high) = 200.0;
        id(i_low)  = 151.0;
        id(c_high) = 150.4;
        id(c_low)  = 55.5;
        id(aqi_color).publish_state("Red");
      } else if (id(pm25).state >= 35.5) {
        id(i_high) = 150.0;
        id(i_low)  = 101.0;
        id(c_high) = 55.4;
        id(c_low)  = 35.5;
        id(aqi_color).publish_state("Orange");
      } else if (id(pm25).state >= 12.1) {
        id(i_high) = 100.0;
        id(i_low)  = 51.0;
        id(c_high) = 35.4;
        id(c_low)  = 12.1;
        id(aqi_color).publish_state("Yellow");
      } else {
        id(i_high) = 50.0;
        id(i_low)  = 0.0;
        id(c_high) = 12.0;
        id(c_low)  = 0.0;
        id(aqi_color).publish_state("Green");
      }
      return round(((id(i_high) - id(i_low))/(id(c_high) - id(c_low))) * (id(pm25).state - id(c_low)) + id(i_low));

text_sensor:
  - platform: template
    name: AQI Color
    icon: mdi:pine-tree-fire
    id: aqi_color
    update_interval: never

One thing to note, my ESPHome implementation doesn’t perform a 24 hour average for the AQI index calculation – I may implement that down the road.
It works GREAT:

The cool thing is, the wiring for this device is even simpler than the BME280 that @Limych shared above. It’s literally three pins (+5V, GND, and RX). Ignore the incorrect BLE CLIENT RADON label. :smiley:

I’m going to build a few of these and put them in and around the house. :+1:

EDIT

One more thing to add here. I bought a Blueair 280i air purifier and it measures VOC and CO2 content. I have had the thing for a week or two and I have been pitting its PM values against the homebrew values but I had no way to crosscheck the VOC nor CO2 values, until now!

I found the CCS811 and it’s supported by ESPHome! This is probably the same sensor used by the 280i that I have because on power-up the device calibrates itself and the CO2 value is stuck at 400, this happens on the 280i as well. The CCS811 is i2c and 3.3V.

According to the ESPHome sheet on the CCS811, if you have temperature and humidity to feed into the device it will give you a more accurate reading so I dropped in a DHT22 and fed the readings to it. The DHT22 is also 3.3V.

Now my breadboard looks like this:

That gives me all of this output:

And here’s the config file for it:

uart:
  rx_pin: 16
  baud_rate: 9600

i2c:
  sda: GPIO21
  scl: GPIO22

globals:
  - id: c_low
    type: float
  - id: c_high
    type: float
  - id: i_low
    type: float
  - id: i_high
    type: float
    
sensor:
  - platform: dht
    pin: GPIO19
    temperature:
      name: "Living Room Temperature"
      id: living_room_temperature
    humidity:
      name: "Living Room Humidity"
      id: living_room_humidity
    update_interval: 60s  
  - platform: ccs811
    eco2:
      name: "Living Room CO2"
      filters:
        - filter_out: 65021
    tvoc:
      name: "Living Room TVOC"
      filters:
        - filter_out: 65021
    address: 0x5A
    update_interval: 60s
    temperature: living_room_temperature
    humidity: living_room_humidity
    # baseline: 0xF4FF
  - platform: pmsx003
    type: PMSX003
    pm_1_0:
      name: "Living Room Particulate Matter <1.0µm Concentration"
      id: living_room_pm_1_0
      accuracy_decimals: 1
      filters:
        - sliding_window_moving_average:
            window_size: ${my_window_size}
            send_every: ${my_send_every}
            send_first_at: ${my_send_first_at}
    pm_2_5:
      name: "Living Room Particulate Matter <2.5µm Concentration"
      id: living_room_pm_2_5
      accuracy_decimals: 1
      filters:
        - sliding_window_moving_average:
            window_size: ${my_window_size}
            send_every: ${my_send_every}
            send_first_at: ${my_send_first_at}
    pm_10_0:
      name: "Living Room Particulate Matter <10.0µm Concentration"
      id: living_room_pm_10_0
      accuracy_decimals: 1
      filters:
        - sliding_window_moving_average:
            window_size: ${my_window_size}
            send_every: ${my_send_every}
            send_first_at: ${my_send_first_at}

  - platform: template
    name: Living Room PM 1.0 rolling 30 minute average
    id: living_room_pm_1_0_rolling_30_minute_average
    unit_of_measurement: µg/m³
    icon: mdi:molecule
    update_interval: 2s
    lambda: >
      return id(living_room_pm_1_0).state;
    filters:
      - sliding_window_moving_average:
          window_size: 900
          send_every: 15
          send_first_at: 15
  - platform: template
    name: Living Room PM 2.5 rolling 30 minute average
    id: living_room_pm_2_5_rolling_30_minute_average
    unit_of_measurement: µg/m³
    icon: mdi:molecule
    update_interval: 2s
    lambda: >
      return id(living_room_pm_2_5).state;
    filters:
      - sliding_window_moving_average:
          window_size: 900
          send_every: 15
          send_first_at: 15
  - platform: template
    name: Living Room PM 10.0 rolling 30 minute average
    id: living_room_pm_10_0_rolling_30_minute_average
    unit_of_measurement: µg/m³
    icon: mdi:molecule
    update_interval: 2s
    lambda: >
      return id(living_room_pm_10_0).state;
    filters:
      - sliding_window_moving_average:
          window_size: 900
          send_every: 15
          send_first_at: 15

  # https://en.wikipedia.org/wiki/Air_quality_index
  - platform: template
    name: Living Room Air Quality Index
    unit_of_measurement: AQI
    icon: mdi:pine-tree-fire
    accuracy_decimals: 0
    lambda: >
      if (id(living_room_pm_2_5_rolling_30_minute_average).state > 500.4) {
        return NAN;
      } else if (id(living_room_pm_2_5_rolling_30_minute_average).state >= 350.5) {
        id(i_high) = 500.0;
        id(i_low)  = 401.0;
        id(c_high) = 500.0;
        id(c_low)  = 350.5;
      } else if (id(living_room_pm_2_5_rolling_30_minute_average).state >= 250.5) {
        id(i_high) = 400.0;
        id(i_low)  = 301.0;
        id(c_high) = 350.4;
        id(c_low)  = 250.5;
        id(living_room_aqi_color).publish_state("Maroon");
      } else if (id(living_room_pm_2_5_rolling_30_minute_average).state >= 150.5) {
        id(i_high) = 300.0;
        id(i_low)  = 201.0;
        id(c_high) = 250.4;
        id(c_low)  = 150.5;
        id(living_room_aqi_color).publish_state("Purple");
      } else if (id(living_room_pm_2_5_rolling_30_minute_average).state >= 55.5) {
        id(i_high) = 200.0;
        id(i_low)  = 151.0;
        id(c_high) = 150.4;
        id(c_low)  = 55.5;
        id(living_room_aqi_color).publish_state("Red");
      } else if (id(living_room_pm_2_5_rolling_30_minute_average).state >= 35.5) {
        id(i_high) = 150.0;
        id(i_low)  = 101.0;
        id(c_high) = 55.4;
        id(c_low)  = 35.5;
        id(living_room_aqi_color).publish_state("Orange");
      } else if (id(living_room_pm_2_5_rolling_30_minute_average).state >= 12.1) {
        id(i_high) = 100.0;
        id(i_low)  = 51.0;
        id(c_high) = 35.4;
        id(c_low)  = 12.1;
        id(living_room_aqi_color).publish_state("Yellow");
      } else {
        id(i_high) = 50.0;
        id(i_low)  = 0.0;
        id(c_high) = 12.0;
        id(c_low)  = 0.0;
        id(living_room_aqi_color).publish_state("Green");
      }
      return round(((id(i_high) - id(i_low))/(id(c_high) - id(c_low))) * (id(living_room_pm_2_5_rolling_30_minute_average).state - id(c_low)) + id(i_low));

text_sensor:
  - platform: template
    name: Living Room AQI Color
    icon: mdi:pine-tree-fire
    id: living_room_aqi_color
    update_interval: never

I put the filter_out values on the VOC and CO2 because the sensor throws out a MAX value when it first fires up. If you’re charting the results, you can’t see the real stuff when it starts coming in. Anyway, here’s to fresh air!! :smiley:

6 Likes

For the benefit of anyone else, after the fact, when using most CCS811 sensor modules:
:bulb: Remember to connect the WAKE pin to GROUND. :bulb:

Otherwise, your CCS811 sensor won’t even show up on the I2C bus scan.

1 Like

Thanks @FredTheFrog, I didn’t run into this issue. I should probably ground mine for good measure…

1 Like

I reviewed four or five different references for different CCS811 sensor modules and code, and it wasn’t until I found one that finally provided that reminder, that my sensor started working as expected. Just thought I’d save someone else a LOT of frustration. :slight_smile:

1 Like

@Limych im using a BME680, can you help in letting me know which sensor values to map to create the IAQ value?
bme680_gas_resistance
bme680_humidity
bme680_pressure
bme680_temperature

thanks

for IAQ (or do you mean AQI?) have a look here BME680 gas resistance values - #15 by jorgenn or have a loook at the new BME680 bsec integration

1 Like

Many thanks to Limych for creating this. I’ll be buying you a coffee.

Just testing this with my PM (HM330X), CO2 & TVOC (SGP30) and temp+humidity (AM2301) sensors.
All on an ESP32 running Tasmota FYI.

Couple of questions, as I’m not a Lovelace master by any means;

  1. Is it possible to remove the ‘IAQI’ units suffix for the index sensor?
  2. Any way to change either the icon, description text, or status text colour for the level sensor, to reflect the IQA rating scale? i.e. green=excellent, red=inadequate.

Thanks again.

Nice component. Would it be possible to take into account ozone and sulfur dioxide?

1 Like

With this setup are you still using the interval so you only turn on measurement for 20s every 2 minutes or do you measure air quality continuous?

My live versions are very close to what I shared previously. I have like six of them all over my house. They measure continuously but I primarily use the rolling data because of the inaccuracy of the instantaneous data. Noise doesn’t help anyone make a decision.

If you look at “official AQI number” reporting, you’ll see that the average is rolled by 24 hours which causes significant delay in reporting… it takes a long time to move the needle when a nasty air front moves in quickly. With a 30 minute rolled average the results are nearly real time by comparison.

I have great visibility to see the quality of the air from my house and now I can align what I see with what is actually happening. Mostly I wanted i know: is it just fog in the valley or is it bad air and we should stay inside? The AQI sensor tells me.

I also use the room-based data to trigger air purifiers that I have in the house should the room’s air quality start to degrade. I throttle the purifier based on how “bad” the AQI measurement is. It works very well.

1 Like

Woah, I just noticed that ESPHome’s integration for PMS5003ST and PMS5003S now have support for formaldehyde! Lately, I’ve been trying to register my 3D printer’s impact to room air quality with the PMS7003 sensors I bought but nothing seems to register – even TVOC/CO2. I know that printing ABS generates formaldehyde (which is pretty much all I print) so I’m going to get a couple of these PMS5003ST sensors and see what it can see. :smiley:

1 Like

I just installed and got this working. I love this component, but I am not a programmer. The Indoor air quality UK component makes use of a function that has been depreciated and will soon no longer function. It looks like there is a replacement available. Is there any chance you will be updating this component before it stops working?

This is the Home assistant log message:

Logger: homeassistant.helpers.frame Source: helpers/frame.py:77 First occurred: 7:50:15 AM (1 occurrences) Last logged: 7:50:15 AM
Detected integration that uses temperature utility. This is deprecated since 2022.10 and will stop working in Home Assistant 2023.4, it should be updated to use unit_conversion.TemperatureConverter instead. Please report issue to the custom integration author for iaquk using this method at custom_components/iaquk/__init__.py, line 355: value = convert_temperature(value, entity_unit, TEMP_CELSIUS)

I would really appreciate your help keeping this component alive.

1 Like

Nice work, but my sensors are coming up as sensor.unnamed_device.
I don’t believe I missed a step in the config, did i?

iaquk:
  livingroom:
    name: "iaq Living Room"
    sources:
      temperature: sensor.woonkamer_temperature
      humidity: sensor.woonkamer_humidity
      co2: sensor.livingsensor_co2_value

Any update on making this work with 2023.6?

Thank you so much @FredTheFrog! With your comment, you saved my day! I spend a couple of days trying to set up the sensor and after connecting the WAKE pin to GROUND, everything worked! Thanks!

1 Like

I’m having problems getting this up and running as it errors on boot, here’s the logs…

Logger: homeassistant.setup
Source: setup.py:251
First occurred: 15:41:53 (1 occurrences)
Last logged: 15:41:53

Setup failed for custom integration 'iaquk': Unable to import component: No module named 'homeassistant.util.temperature'
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/setup.py", line 251, in _async_setup_component
    component = integration.get_component()
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/loader.py", line 814, in get_component
    ComponentProtocol, importlib.import_module(self.pkg_path)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1147, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 940, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/config/custom_components/iaquk/__init__.py", line 30, in <module>
    from homeassistant.util.temperature import convert as convert_temperature
ModuleNotFoundError: No module named 'homeassistant.util.temperature'

Googling the error gets me another thread about another integration, and it looks like that module was deprecated a year ago, not sure if that helps?

:sob:

Home Assistant Core
2024-01-07 19:15:38.505 WARNING (SyncWorker_2) [homeassistant.loader] We found a custom integration iaquk which has not been tested by Home Assistant. This component might cause stability problems, be sure to disable it if you experience issues with Home Assistant
2024-01-07 19:15:41.917 WARNING (MainThread) [homeassistant.const] TEMP_CELSIUS was used from iaquk, this is a deprecated constant which will be removed in HA Core 2025.1. Use UnitOfTemperature.CELSIUS instead, please create a bug report at https://github.com/Limych/ha-iaquk/issues
2024-01-07 19:15:41.921 WARNING (MainThread) [homeassistant.const] TEMP_FAHRENHEIT was used from iaquk, this is a deprecated constant which will be removed in HA Core 2025.1. Use UnitOfTemperature.FAHRENHEIT instead, please create a bug report at https://github.com/Limych/ha-iaquk/issues
2024-01-07 19:15:41.921 ERROR (MainThread) [homeassistant.setup] Setup failed for custom integration 'iaquk': Unable to import component: No module named 'homeassistant.util.temperature'
File "/config/custom_components/iaquk/__init__.py", line 30, in <module>

Just installed the latest update and it’s all working again! :grinning:

1 Like

I have the same issue. Did you find a solution?