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.
I’m going to build a few of these and put them in and around the house.
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!!