>>> Join Us in the Fight Against Air Pollution

AirGradient Forum

Air Quality Level

Hey, I ordered the DIY basic kit and got it working. When I flashed the air gradient with the Air Gradient firmware via browser, I think I saw a value for Air Quality Level (AQL or similar) on the display. Is this a value that is formed across all values that the Air Grandient measures? If so, I would like to know how the value is calculated/formed. I use the Air Gradient in Home Assistant with the ESP Home Integration and would also like it there.

It is the PM2.5 US AQI and only based on PM2.5.

This is the calculation code:

// Calculate PM2.5 US AQI
int PM_TO_AQI_US(int pm02) {
  if (pm02 <= 12.0) return ((50 - 0) / (12.0 - .0) * (pm02 - .0) + 0);
  else if (pm02 <= 35.4) return ((100 - 50) / (35.4 - 12.0) * (pm02 - 12.0) + 50);
  else if (pm02 <= 55.4) return ((150 - 100) / (55.4 - 35.4) * (pm02 - 35.4) + 100);
  else if (pm02 <= 150.4) return ((200 - 150) / (150.4 - 55.4) * (pm02 - 55.4) + 150);
  else if (pm02 <= 250.4) return ((300 - 200) / (250.4 - 150.4) * (pm02 - 150.4) + 200);
  else if (pm02 <= 350.4) return ((400 - 300) / (350.4 - 250.4) * (pm02 - 250.4) + 300);
  else if (pm02 <= 500.4) return ((500 - 400) / (500.4 - 350.4) * (pm02 - 350.4) + 400);
  else return 500;
};
1 Like

Here is a sensor for home assistant using the code above:

sensor:
  - platform: template
    sensors:
      pm25_aqi:
        value_template: >-
          {% set pm02 = states('sensor.pm02') | float %}
          {% if pm02 <= 12.0 %}
            {{ ((50 - 0) / (12.0 - 0) * (pm02 - 0) + 0) | int }}
          {% elif pm02 <= 35.4 %}
            {{ ((100 - 50) / (35.4 - 12.0) * (pm02 - 12.0) + 50) | int }}
          {% elif pm02 <= 55.4 %}
            {{ ((150 - 100) / (55.4 - 35.4) * (pm02 - 35.4) + 100) | int }}
          {% elif pm02 <= 150.4 %}
            {{ ((200 - 150) / (150.4 - 55.4) * (pm02 - 55.4) + 150) | int }}
          {% elif pm02 <= 250.4 %}
            {{ ((300 - 200) / (250.4 - 150.4) * (pm02 - 150.4) + 200) | int }}
          {% elif pm02 <= 350.4 %}
            {{ ((400 - 300) / (350.4 - 250.4) * (pm02 - 250.4) + 300) | int }}
          {% elif pm02 <= 500.4 %}
            {{ ((500 - 400) / (500.4 - 350.4) * (pm02 - 350.4) + 400) | int }}
          {% else %}
            500
          {% endif %}
        unit_of_measurement: "AQI"

Make sure to replace sensor.pm02 with the entity ID of the sensor that provides the PM2.5 value you want to use in the calculation.

1 Like

I will add my code for esphome. I did follow the complete recommendation by only giving AQI results on a 24hr average and only after 18hr of data. Because short periods of high amount of parcticles shouldn’t spike the readings. Also the AQI levels are different around the globe, choose the level what fits your region the most.

  - platform: copy
    source_id: pm25
    id: pm_2_5
    name: "${upper_devicename} PM <2.5µm Average 24h"
    accuracy_decimals: 0
    filters:
    - sliding_window_moving_average:
        window_size: 480 #every 3 minutes for 24 hours
        send_every: 20 #hourly
        send_first_at: 20
    on_value:
      lambda: |-
        static int i = 0;
        i++;
        if(i>=18){ // Only publish after 18 hours
          // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
          if (id(pm_2_5).state < 12.0) {
            // good
            id(aqi_text).publish_state("Good");
            id(pm_2_5_aqi).publish_state((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
          } else if (id(pm_2_5).state < 35.4) {
            // moderate
            id(aqi_text).publish_state("Moderate");
            id(pm_2_5_aqi).publish_state((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
          } else if (id(pm_2_5).state < 55.4) {
            // Unhealthy for Sensitive Groups
            id(aqi_text).publish_state("Unhealthy for Sensitive Groups");
            id(pm_2_5_aqi).publish_state((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
          } else if (id(pm_2_5).state < 150.4) {
            // unhealthy
            id(aqi_text).publish_state("Unhealthy");
            id(pm_2_5_aqi).publish_state((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
          } else if (id(pm_2_5).state < 250.4) {
            // very unhealthy
            id(aqi_text).publish_state("Very Unhealthy");
            id(pm_2_5_aqi).publish_state((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
          } else if (id(pm_2_5).state < 350.4) {
            // hazardous
            id(aqi_text).publish_state("Hazardous");
            id(pm_2_5_aqi).publish_state((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
          } else if (id(pm_2_5).state < 500.4) {
            // hazardous 2
            id(aqi_text).publish_state("Hazardous");
            id(pm_2_5_aqi).publish_state((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
          }
        }
1 Like