On device PM2.5 to AQI conversion with ESPHome

Recently flashed my AirGradient Pro with ESPhome so I can send my data to Home Assistant.
One thing I really miss from the original firmware was the AQI (PM2.5) reading on the OLED.

Was wondering if anyone could shed some light on how to do this within ESPHome?
It seems a little tricky as it’s not a straight forward calculation, there’s detail on how to do simple conversions on the ESPHome documentation but not something like a lookup table.

Has anyone managed this and is willing to share how they did it?

Cheers

Update:
I have managed to do this by calculating the AQI value using a Home Assistant Sensor Template and passing this back to the monitor to display on the OLED. Have attached the code for anyone trying to do the same thing.

Home Assistant Configuration.yaml:
(Originally from here)

template:
- sensor:
    - name: "AQI (pm2.5)"
      state: >-
        {% macro aqi(val, val_l, val_h, aqi_l, aqi_h) -%}
          {{(((aqi_h-aqi_l)/(val_h - val_l) * (val - val_l)) + aqi_l)|round(0)}}
        {%- endmacro %}
        {% set v = states('sensor.airgradient_pro_particulate_matter_2_5um_concentration')|round(1) %}
        {% if v <= 12.0 %}
           {{aqi(v, 0, 12.0, 0, 50)}}
        {% elif 12.0 < v <= 35.4 %}
           {{aqi(v, 12.1, 35.4, 51, 100)}}
        {% elif 35.4 < v <= 55.4 %}
           {{aqi(v, 35.5, 55.4, 101, 150)}}
        {% elif 55.4 < v <= 150.5 %}
           {{aqi(v, 55.5, 150.4, 151, 200)}}
        {% elif 150.4 < v <= 250.4 %}
           {{aqi(v, 150.4, 250.4, 201, 300)}}
        {% elif 250.5 < v <= 500.4 %}
           {{aqi(v, 250.5, 500.4, 301, 500)}}
        {% else %}
           Very Very Bad
        {% endif %}
      unit_of_measurement: AQI
      device_class: aqi

espHome yaml:

  - platform: homeassistant
    id: AQI
    entity_id: "sensor.aqi_pm2_5"

Then you just need to pass that value to a the screen as you would for an internal value

1 Like

Looks good. I have my AQI calculated on the esp itself with a lambda on a running average of 24 hours. So no short high concentrations spike the AQI as it should. Prolonged high concentrations is bad so it will go up over time. Also I have added a minimum of 18 hours before publishing the value.

Here is some more information Air quality index - Wikipedia

  - 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);
          }
        }

  - platform: template
    name: "${upper_devicename} PM <2.5 AQI"
    unit_of_measurement: "aqi"
    icon: "mdi:air-filter"
    accuracy_decimals: 0
    id: pm_2_5_aqi
1 Like

Thank you both for your contributions. I was doing the same a few months ago and ended up putting the conversion in a separate file and just calling it from the ESPHome yaml. This is much cleaner and no need to keep track of 2 files anymore.

My implementation for reference: GitHub - handro123/AirGradient-DIY-Pro-ESPHome