PurpleAir Air Quality Sensor

Yeah my understanding from that article was that there was a new conversion on the way, in addition to LRAPA, which is what I tried to implement.

Thanks for the custom component! Very useful.

I forked the copy @johnboiles put on GitHub to manually apply the PurpleAir LRAPA conversion to the air_quality_index values. Probably not something people would want to use outside of the specific wood fire situation.

Thanks for making it so easy with the config flow and support for multiple sensors. It made it trivial to then do a template sensor average AQI of the stations near me and use that as a trigger for notifications/automations.

1 Like

FYI. LRAPA is specific to the region by the folks that made that conversion. I’m curios how it applies to other areas. Has anyone found information on LRAPA accuracy in different areas? Also I made a blog post on how to DIY a PurpleAir. DIY PurpleAir Sensor

2 Likes

Just coming into this thread for the first time because I’m strongly considering building a quick sensor for this as I’m in the midst of wildfire smoke (being in Oakland, CA). Anything I need to know about consuming this data? I saw something about possibly applying LRAPA, but is there anything I would need to do other than consuming https://www.purpleair.com/json?show=19645?

LRAPA acknowledges that the correction factor developed by LRAPA will not necessarily transfer to other airsheds. The PM2.5 levels in our area are primarily driven by winter time wood smoke from home wood heating. The correction factor is likely heavily dependent on the makeup of PM2.5 aerosol in our area.

Source: LRAPA PurpleAir Monitor Correction Factor History

I don’t think the geographic location (“airshed”) is the important part, but the type of pollution. The LRAPA correction was developed in an area where the primary pollutant is “wood smoke from home wood heating,” so it should be accurate for that pollutant. The question becomes how applicable is it to wildfire smoke which is the result of combusting not only wood, but grasses, leaves, other wilderness biomass and to a lesser extent structures, vehicles, etc.

The EPA, though, does have a correction formula specifically for wildfire smoke, which you can read about here and here.

The EPA correction is probably the most accurate one for the wildfire smoke situation in the western USA.

That’s encouraging news, I’m also in the bay area and I’m hoping to get something working.

I tried getting the WAQI integration working but no luck so far. Hopefully this can be more tightly integrated into HA as I fear this may be needed quite a bit this season :frowning:

Hi - I have installed the integration… but I can’t seem to find the ‘get this widget’ for my local air sensor on PurpleAir. I have tried a couple of browsers now. Any advice?

If you select the sensor you are interested in on the map, and you should see in the url “select=[some number]”

You can then browse to the URL “https://www.purpleair.com/json?show=[some number]” to see the JSON, and use that URL when setting up the integration.

2 Likes

Amazing, thank you!

I’m glad my work could be of some use! I don’t use HACS and prefer not to use GitHub if I can avoid it, so knowing they rely on one another is useful. I’ll move mine over on the weekend. Thank you for doing that to help get it out to more people! That’s why I made sure it was open in the first place, it’s been quite hectic the last couple weeks, so thanks for taking the torch!

Thanks for going the extra mile! I heard about LARPA just as I was wrapping things up and wasn’t able to find a formula. Supposedly AQandU is supposed to be better than LARPA (one of the articles linked mentioned that as well). Right now, the wife and I live with the fact that it reads a bit higher than normal for wildfire smoke (we’re in Colorado, so we get both the smoke from the west and our own large fires as well) so we treat it as a cautious number, which helps us avoid it more than not. (I also show the µg/m³ value as well, and it seems that around 22 µg/m³ is equivalent to smoking a cigarette, 44 µg/m³ is 2, etc.).

For readings in the last couple weeks of wildfire smoke in San Francisco, I found that PurpleAir’s LRAPA generally closely aligned to the AirNow EPA better than AQandU did, but I can imagine results may vary depending on the types of particulates.

Might have been mentioned already, but I noticed AirNow made a Fire & Smoke map that utilizes what seem like selected PurpleAir sites, applying some kind of correction factor that also seems to most closely match PurpleAir’s LRAPA correction. I haven’t looked through the JS to see if it might be the same correction.

I’m curious how they choose which sites to feature on the Fire & Smoke map. They might do some sort of data sanity & reliability check like Weather Underground does (used to do?) for PWSes, but I wouldn’t be surprised if it was just a random sampling.

Hopefully you got something working for yourself already, but in case not: the easiest way is just to add in @gibwar’s custom component, either manually or via HACS. Since you’re interested for wildfire smoke purposes, you’ll probably want to apply the LRAPA correction to the AQI, which will be done automatically if you use the minor fork I mentioned earlier. Note that the raw pm2.5 readings aren’t adjusted, just the resulting AQI, but that’s probably all you need.

And yeah, just paste in that link when adding the integration via the config flow. @gibwar also made it super easy to add multiple instances that way, which makes it easy to make a sensor for getting an average of PurpleAir sensors near you.

I’m unable to install the integration via the gui. I added & installed the github url to HACS repo. I see dir homeassistant/custom_components/purpleair
I restarted HA, more than once.
But when I go to Configuration > Integrations > (+)
I don’t see anything containing “pur” (screenshot):

What am I missing? I have added lots of other integrations via the gui (and yaml) before.

HA ver 0.114.4
Also tried manually moving the purpleair dir from the .zip file into my custom_components dir and restarting HA. Same issue.

URL i added to HACS as a repository:

For now, I just pasted the yaml from Post #1 in this thread into configuration.yaml and changed the url

1 Like

I wish I’d read the entire thread before I went hunting to find the ID of the station nearest to me. I basically tried random IDs increasing from the 37027 one earlier in this thread and looked at the create time in the json result. I knew the sensor I wanted was created 8/31/20 and I knew the name from the purple air map. I managed to work my way up in leaps and found 63835 created 8/31/20. A little script later searching the ones numerically up and down where the create date was still 8/31 turned up the one I wanted.

FYI - 435 new IDs were created on 8/31 - most often in pairs as each PurpleAir device has two sensors, each with a different ID.

I thought I’d share my icon choices for the items that didn’t have anything specific. I’m curious what others may have used.
AQI

### customize.yaml
### AQI 
sensor.purpleair_description:
  icon: mdi:lungs
sensor.purpleair_aqi:
  icon: mdi:chemical-weapon
sensor.purpleair_pm25:
  icon: mdi:grain

For those that utilize Grafana:

{
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "-- Grafana --",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "gnetId": null,
  "graphTooltip": 0,
  "id": 1,
  "links": [],
  "panels": [
    {
      "datasource": "HomeAssistant",
      "description": "",
      "fieldConfig": {
        "defaults": {
          "custom": {},
          "mappings": [],
          "max": 500,
          "min": 0,
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "green",
                "value": 0
              },
              {
                "color": "#EAB839",
                "value": 51
              },
              {
                "color": "orange",
                "value": 101
              },
              {
                "color": "red",
                "value": 151
              },
              {
                "color": "purple",
                "value": 201
              },
              {
                "color": "dark-purple",
                "value": 301
              }
            ]
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 12,
        "y": 0
      },
      "id": 6,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": [
            "lastNotNull"
          ],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "7.2.0",
      "targets": [
        {
          "alias": "",
          "groupBy": [
            {
              "params": [
                "$__interval"
              ],
              "type": "time"
            },
            {
              "params": [
                "null"
              ],
              "type": "fill"
            }
          ],
          "measurement": "AQI",
          "orderByTime": "ASC",
          "policy": "default",
          "refId": "A",
          "resultFormat": "time_series",
          "select": [
            [
              {
                "params": [
                  "value"
                ],
                "type": "field"
              },
              {
                "params": [],
                "type": "last"
              }
            ]
          ],
          "tags": []
        }
      ],
      "timeFrom": null,
      "timeShift": null,
      "title": "Air Quaility",
      "transparent": true,
      "type": "gauge"
    },
    {
      "aliasColors": {
        "AQI": "blue"
      },
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "HomeAssistant",
      "description": "",
      "fieldConfig": {
        "defaults": {
          "custom": {},
          "mappings": [],
          "max": 500,
          "min": 0,
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "green",
                "value": 0
              },
              {
                "color": "#EAB839",
                "value": 51
              },
              {
                "color": "orange",
                "value": 101
              },
              {
                "color": "red",
                "value": 151
              },
              {
                "color": "purple",
                "value": 201
              },
              {
                "color": "dark-purple",
                "value": 301
              }
            ]
          }
        },
        "overrides": []
      },
      "fill": 1,
      "fillGradient": 0,
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 12,
        "y": 8
      },
      "hiddenSeries": false,
      "id": 7,
      "legend": {
        "avg": false,
        "current": false,
        "max": false,
        "min": false,
        "show": true,
        "total": false,
        "values": false
      },
      "lines": true,
      "linewidth": 1,
      "nullPointMode": "null",
      "options": {
        "alertThreshold": true
      },
      "percentage": false,
      "pluginVersion": "7.2.0",
      "pointradius": 2,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "spaceLength": 10,
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "alias": "AQI",
          "groupBy": [
            {
              "params": [
                "$__interval"
              ],
              "type": "time"
            },
            {
              "params": [
                "none"
              ],
              "type": "fill"
            }
          ],
          "measurement": "AQI",
          "orderByTime": "ASC",
          "policy": "default",
          "refId": "A",
          "resultFormat": "time_series",
          "select": [
            [
              {
                "params": [
                  "value"
                ],
                "type": "field"
              },
              {
                "params": [],
                "type": "last"
              }
            ]
          ],
          "tags": []
        }
      ],
      "thresholds": [
        {
          "colorMode": "ok",
          "fill": false,
          "line": true,
          "op": "gt",
          "value": 0,
          "yaxis": "left"
        },
        {
          "colorMode": "custom",
          "fill": false,
          "fillColor": "rgba(51, 162, 229, 0.2)",
          "line": true,
          "lineColor": "#FADE2A",
          "op": "gt",
          "value": 51,
          "yaxis": "left"
        },
        {
          "colorMode": "custom",
          "fill": false,
          "fillColor": "rgba(51, 162, 229, 0.2)",
          "line": true,
          "lineColor": "#FF9830",
          "op": "gt",
          "value": 101,
          "yaxis": "left"
        },
        {
          "colorMode": "custom",
          "fill": false,
          "fillColor": "rgba(51, 162, 229, 0.2)",
          "line": true,
          "lineColor": "#F2495C",
          "op": "gt",
          "value": 151,
          "yaxis": "left"
        },
        {
          "colorMode": "custom",
          "fill": false,
          "fillColor": "rgba(51, 162, 229, 0.2)",
          "line": true,
          "lineColor": "#B877D9",
          "op": "gt",
          "value": 201,
          "yaxis": "left"
        },
        {
          "colorMode": "custom",
          "fill": false,
          "fillColor": "rgba(51, 162, 229, 0.2)",
          "line": true,
          "lineColor": "#8F3BB8",
          "op": "gt",
          "value": 301,
          "yaxis": "left"
        }
      ],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Air Quaility",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "transparent": true,
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    }
  ],
  "refresh": "5m",
  "schemaVersion": 26,
  "style": "dark",
  "tags": [],
  "templating": {
    "list": []
  },
  "time": {
    "from": "now-6h",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "Environmental",
  "uid": "hmhTkNOMk",
  "version": 10
}
2 Likes

How is the local API working for you?
I will be getting a PurpleAir device probably a PurpleAir PA-II-SD and a maybe a PurpleAir PA-I-Indoor.

I’m curious what model you have and if anyone else knows if the local API is the same for all the devices (at lease for the sensors that device contains).

Just wanted to share how I’m using the PurpleAir sensors. Like many of you I’m just using the REST sensor. My neighbor has an outdoor PurpleAir so I use the public API to grab that data. I then bought an indoor PurpleAir and use the local API to get that data.

For the outdoor sensor I use different data than most. I found using the PM2_5Value, which is using the real-time data, made the sensor too “wobbly.” Since I had actions tied to it’s value weird things would happen when there was a sudden change in the data. I found buried in the json data the 10 minute average of the sensor. Using that helped smooth out the data. Unfortunately, the data is not proper json. Its actually a string. But with a bit of string parsing in the template I was able to get at the value. I also decided to assign the AQI value to a variable in the template, for two reasons, to make it more readable, and easier to switch back to the real-time value if I wanted.

- platform: rest
  name: 'PurpleAir'

  resource: https://www.purpleair.com/json?show=19269
  scan_interval: 60
  force_update: true

  value_template: >
    {% macro calcAQI(Cp, Ih, Il, BPh, BPl) -%}
      {{ (((Ih - Il)/(BPh - BPl)) * (Cp - BPl) + Il)|round }}   
    {%- endmacro %}
    # Use the 10m average of PM25 and assign it to variable
    {% set pm25 = value_json.results[0].Stats.split(',')[1].split(':')[1] %}
    {% if (pm25|float) > 1000 %}
      invalid
    {% elif (pm25|float) > 350.5 %}
      {{ calcAQI((pm25|float), 500.0, 401.0, 500.0, 350.5) }}
    {% elif (pm25|float) > 250.5 %}
      {{ calcAQI((pm25|float), 400.0, 301.0, 350.4, 250.5) }}
    {% elif (pm25|float) > 150.5 %}
      {{ calcAQI((pm25|float), 300.0, 201.0, 250.4, 150.5) }}
    {% elif (pm25|float) > 55.5 %}
      {{ calcAQI((pm25|float), 200.0, 151.0, 150.4, 55.5) }}
    {% elif (pm25|float) > 35.5 %}
      {{ calcAQI((pm25|float), 150.0, 101.0, 55.4, 35.5) }}
    {% elif (pm25|float) > 12.1 %}
      {{ calcAQI((pm25|float), 100.0, 51.0, 35.4, 12.1) }}
    {% elif (pm25|float) >= 0.0 %}
      {{ calcAQI((pm25|float), 50.0, 0.0, 12.0, 0.0) }}
    {% else %}
      invalid
    {% endif %}

  unit_of_measurement: "AQI"
  json_attributes:
    - Stats
    - PM2_5Value
    - LastSeen
    - LastUpdateCheck
    - Label
  json_attributes_path: "$.results[0]"

For the indoor sensor that data that is returned is the from the local API is different. I have access to the AQI directly but it is a real-time value as far as I can tell. PurpleAir says they are updating the FAQ to include details of the local API, so hopefully I can get a better definition of what the value actually is.

- platform: rest
  name: 'AQI Indoors'
  resource: http://<Internal IP>/json
  scan_interval: 30
  force_update: true

  value_template: '{{ value_json["pm2.5_aqi"] }}'
  unit_of_measurement: "AQI"

I also tied the indoor AQI to the fan of the air handler in my HVAC system. It also gave me a use case for the new wait_for_trigger action options for automation. It’s not working quite right. I think it has to do with getting to the timeout value. But that is a discussion for another thread.

- alias: HVAC Dirty Air Cycle
  trigger:
    platform: numeric_state
    entity_id: sensor.aqi_indoors
    above: 50
  condition:
    - condition: state
      entity_id: fan.whole_house
      state: 'off'
    - condition: numeric_state
      entity_id: sensor.open_window_count
      below: 1
  action:
    - service: climate.set_fan_mode
      data:
        entity_id: climate.my_ecobee
        fan_mode: 'on'
    - wait_for_trigger:
        - platform: numeric_state
          entity_id: sensor.aqi_indoors
          below: 20
      timeout:
        minutes: 60
      continue_on_timeout: true
    - service: climate.set_fan_mode
      data:
        entity_id: climate.my_ecobee
        fan_mode: 'auto'
3 Likes