PurpleAir Air Quality Sensor

Thoughts on this one?
I think this is my final.
Icons change based on air quality.
Font color inverts from black to white as levels get higher.
Colors between the three sections are more closely matched now.

type: vertical-stack
cards:
  - type: 'custom:button-text-card'
    title: |
      [[[ return states["sensor.purpleair_description"].state ]]]
    subtitle: Air Quaility
    icon_size: 55
    icon_color: |
      [[[
        if(states["sensor.purpleair_aqi"].state >= 300){
          return 'white';
        } else if(states["sensor.purpleair_aqi"].state >= 200){
          return 'white';
        } else if(states["sensor.purpleair_aqi"].state >= 150){
          return 'white';
        } else if(states["sensor.purpleair_aqi"].state >= 100){
          return 'white';
        } else if(states["sensor.purpleair_aqi"].state >= 50){
          return 'black';
        } else{
          return 'black';
        }
      ]]]
    icon: |
      [[[
        if(states["sensor.purpleair_aqi"].state >= 300){
          return 'mdi:emoticon-dead';
        } else if(states["sensor.purpleair_aqi"].state >= 200){
          return 'mdi:emoticon-cry';
        } else if(states["sensor.purpleair_aqi"].state >= 150){
          return 'mdi:emoticon-sad';
        } else if(states["sensor.purpleair_aqi"].state >= 100){
          return 'mdi:emoticon-confused';
        } else if(states["sensor.purpleair_aqi"].state >= 50){
          return 'mdi:emoticon-neutral';
        } else{
          return 'mdi:emoticon-excited';
        }
      ]]]
    font_color: |
      [[[
        if(states["sensor.purpleair_aqi"].state >= 300){
          return 'white';
        } else if(states["sensor.purpleair_aqi"].state >= 200){
          return 'white';
        } else if(states["sensor.purpleair_aqi"].state >= 150){
          return 'white';
        } else if(states["sensor.purpleair_aqi"].state >= 100){
          return 'white';
        } else if(states["sensor.purpleair_aqi"].state >= 50){
          return 'black';
        } else{
          return 'black';
        }
      ]]]
    large: true
    background_color: |
      [[[
        if(states["sensor.purpleair_aqi"].state >= 300){
          return '#731425';
        } else if(states["sensor.purpleair_aqi"].state >= 200){
          return '#8C1A4B';
        } else if(states["sensor.purpleair_aqi"].state >= 150){
          return '#EA3324';
        } else if(states["sensor.purpleair_aqi"].state >= 100){
          return '#EF8533';
        } else if(states["sensor.purpleair_aqi"].state >= 50){
          return '#FFFF55';
        } else{
          return '#68FF43';
        }
      ]]]
  - type: 'custom:canvas-gauge-card'
    card_height: 300
    entity: sensor.purpleair_aqi
    name: ''
    gauge:
      type: radial-gauge
      title: AQI
      width: 300
      height: 300
      minValue: 0
      maxValue: 500
      startAngle: 40
      ticksAngle: 280
      valueBox: true
      majorTicks:
        - '0'
        - '50'
        - '100'
        - '150'
        - '200'
        - '250'
        - '300'
        - '350'
        - '400'
        - '450'
        - '500'
      minorTicks: 10
      strokeTicks: true
      highlights:
        - from: 0
          to: 50
          color: 'rgba(104, 225, 67, .75)'
        - from: 50
          to: 100
          color: 'rgba(255, 255, 85, .75)'
        - from: 100
          to: 150
          color: 'rgba(239, 133, 51, .75)'
        - from: 150
          to: 200
          color: 'rgba(234, 51, 36, .75)'
        - from: 200
          to: 300
          color: 'rgba(140, 26, 75, .75)'
        - from: 300
          to: 500
          color: 'rgba(115, 20, 37, .75)'
      borders: 'no'
      needleType: arrow
      needleWidth: 4
      needleCircleSize: 7
      needleCircleOuter: true
      needleCircleInner: false
      animationDuration: 1500
      animationRule: linear
      valueBoxBorderRadius: 10
      colorValueBoxRect: '#222'
      colorValueBoxRectEnd: '#333'
      valueDec: 0
      valueInt: 0
  - type: 'custom:mini-graph-card'
    entities:
      - sensor.purpleair_aqi
    unit: AQI
    name: ' '
    icon: blank
    show:
      fill: true
      legend: false
      labels: false
      name: true
      points: false
      name_adaptive_color: true
      icon_adaptive_color: true
      show_legend: false
    font_size: 75
    line_width: 3
    points_per_hour: 4
    hours_to_show: 24
    color_thresholds:
      - value: 0
        color: '#68FF43'
      - value: 50
        color: '#68FF43'
      - value: 50.5
        color: '#FFFF55'
      - value: 100
        color: '#FFFF55'
      - value: 100.5
        color: '#EF8533'
      - value: 150
        color: '#EF8533'
      - value: 150.5
        color: '#EA3324'
      - value: 200
        color: '#EA3324'
      - value: 200.5
        color: '#8C1A4B'
      - value: 300
        color: '#8C1A4B'
      - value: 300.5
        color: '#731425'
2 Likes

https://community.home-assistant.io/t/lovelace-button-text-card/210687/8?u=glennha

Examples of each air quality.

3 Likes

Sheesh, we’re all doing the same things. Life in CA I guess? So I have a pms5300 on an esphome and it works great. I get the raw values In the log but am struggling with how to get to the AQI conversion. My esphome has a sensor as follows:

  - platform: pmsx003
    type: PMSX003
    pm_1_0:
      filters:
        #- throttle: 60s
        - delta: 2.0
        - sliding_window_moving_average:
            window_size: 45
      name: sensornode_airquality_pm_1_0
      #name: "Particulate Matter <1.0µm Concentration"
    pm_2_5:
      filters:
        #- throttle: 60s
        - delta: 2.0
        - sliding_window_moving_average:
            window_size: 45
        name: sensornode_airquality_pm_2_5

sensornode_airquality_pm_2_5 Contains the 2.5pm value, Now, how do I pump that result into an AQI conversion calculator? I’m so Close but so confused! A template? In my sensor.yaml file? I see your reference to the esphome mods for the HM3301 but I don’t know how to use that.

UPDATE: I figured it out after sleeping on it. Create a new sensor based on a Template:
sensornode_airquality_pm_2_5 is defined in esphome.

Boy, is yaml friggin fussy. WTH is there no line #'s for errors in the template “developer” tab??? you’d think that would be painfully obvious to anyone who has ever tried to debug more than 3 lines of template code!!!

#sensor.yaml:
#
#-----------------------------
  - platform: template
    sensors:
      inside_aqi:
        friendly_name: 'Inside AQI Calc'
        value_template: >-
          {% macro calcAQI(Cp, Ih, Il, BPh, BPl) -%}
          {{ (((Ih - Il)/(BPh - BPl)) * (Cp - BPl) + Il)|round }}
          {%- endmacro %}
          {% if (states('sensor.sensornode_airquality_pm_2_5')|float) > 1000 %}
           invalid
          {% elif (states('sensor.sensornode_airquality_pm_2_5')|float) > 350.5 %}
            {{ calcAQI((states('sensor.sensornode_airquality_pm_2_5')|float), 500.0, 401.0, 500.0, 350.5) }}
          {% elif (states('sensor.sensornode_airquality_pm_2_5')|float) > 250.5 %}
            {{ calcAQI((states('sensor.sensornode_airquality_pm_2_5')|float), 400.0, 301.0, 350.4, 250.5) }}
          {% elif (states('sensor.sensornode_airquality_pm_2_5')|float) > 150.5 %}
            {{ calcAQI((states('sensor.sensornode_airquality_pm_2_5')|float), 300.0, 201.0, 250.4, 150.5) }}
          {% elif (states('sensor.sensornode_airquality_pm_2_5')|float) > 55.5 %}
            {{ calcAQI((states('sensor.sensornode_airquality_pm_2_5')|float), 200.0, 151.0, 150.4, 55.5) }}
          {% elif (states('sensor.sensornode_airquality_pm_2_5')|float) > 35.5 %}
            {{ calcAQI((states('sensor.sensornode_airquality_pm_2_5')|float), 150.0, 101.0, 55.4, 35.5) }}
          {% elif (states('sensor.sensornode_airquality_pm_2_5')|float) > 12.1 %}
            {{ calcAQI((states('sensor.sensornode_airquality_pm_2_5')|float), 100.0, 51.0, 35.4, 12.1) }}
          {% elif (states('sensor.sensornode_airquality_pm_2_5')|float) >= 0.0 %}
            {{ calcAQI((states('sensor.sensornode_airquality_pm_2_5')|float), 50.0, 0.0, 12.0, 0.0) }}
          {% else %}
            invalid
          {% endif %}
        entity_id: sensor.inside_aqi

this defines a new sensor called sensor.inside_aqi that you can use in lovelace
image

type: vertical-stack
cards:
  - type: entities
    entities:
      - sensor.sensornode_airquality_humidity
      - sensor.sensornode_airquality_pressure
      - sensor.sensornode_airquality_temp
      - sensor.sensornode_airquality_pm_2_5
      - sensor.inside_aqi
title: Inside weather
1 Like

This is my first rest sensor, so perhaps this is unrelated, but I’m receiving empty responses from PurpleAir:

2020-11-08 12:21:57 ERROR (MainThread) [homeassistant.components.rest.data] Error fetching data: https://www.purpleair.com/json?show=55831 failed with 
2020-11-08 12:21:57 DEBUG (MainThread) [homeassistant.components.rest.sensor] Data fetched from resource: None
2020-11-08 12:21:57 WARNING (MainThread) [homeassistant.components.rest.sensor] Empty reply found when expecting JSON data

I’ve tried the URLs in the various examples in this thread to similar effect, as well as HTTP instead of HTTPS. All the URLs output the expected results when used with curl.

Is there some setup here I’m missing?

- platform: rest
  name: purpleair
  resource: https://www.purpleair.com/json?show=55831
  scan_interval: 600
  device_class: timestamp
  value_template: >
    {{ state_attr('sensor.purpleair', 'results')[0]['LastSeen'] }}
  json_attributes:
    - results

I finally received my outdoor sensor, thanks for posting your rest sensor. Coping yours saved me a plethora of time. I did make some modifications but the bulk is a copy of yours.

I added the description template and removed memory info. I’m debating making an average of the A & B but probably won’t.

I am in the process of DewPoint displays that mimic the AQI displays (graphs).
I just need to add the following to my config when I get the chance:

- platform: template
  sensors:
    purpleair_dewpoint_description:
      friendly_name: 'PurpleAir DewPoint Description'
      value_template: >
        {% if (states('sensor.purpleair_dewpoint')|float) >= 75.5 %}
          Miserable
        {% elif (states('sensor.purpleair_dewpoint')|float) >= 70.5 %}
          Oppressive
        {% elif (states('sensor.purpleair_dewpoint')|float) >= 65.5 %}
          Uncomfortable
        {% elif (states('sensor.purpleair_dewpoint')|float) >= 60.5 %}
          Getting Sticky
        {% elif (states('sensor.purpleair_dewpoint')|float) >= 55.5 %}
          Comfortable
        {% elif (states('sensor.purpleair_dewpoint')|float) >= 0.0 %}
          Pleasant
        {% else %}
          undefined
        {% endif %}
      entity_id: sensor.purpleair

I’m still looking at which icons to associate with each category but it will probably be close to what I use for Air Quality. (I am open to suggestions).

The following is mostly done, only the top ‘Comfort Level’ is incomplete awaiting the above yaml.


I will post the finish layout yaml when complete.

Comfort Level Indicator:

https://community.home-assistant.io/t/dewpoint-comfort-level-display/253344

I may be taking this to far…
I have graphs for every particle level, more than I can fit on one screen at a time.
The color change levels come from the PurpleAir Map.

type: vertical-stack
cards:
  - type: 'custom:mini-graph-card'
    entities:
      - sensor.purpleair_p_0_3_um_a
    unit: PM 0.3
    name: Ultrafine Particles 0.3 (A) (24hr)
    icon: 'mdi:grain'
    hour24: true
    show:
      fill: true
      legend: false
      labels: false
      state: true
      icon: true
      extrema: true
      average: true
      name: true
      points: false
      name_adaptive_color: true
      icon_adaptive_color: true
      show_legend: false
    font_size: 75
    line_width: 3
    points_per_hour: 4
    hours_to_show: 24
    color_thresholds_transition: hard
    color_thresholds:
      - value: 0
        color: '#68FF43'
      - value: 1000
        color: '#FFFF55'
      - value: 3000
        color: '#EF8533'
      - value: 10000
        color: '#EA3324'
      - value: 20000
        color: '#8C1A4B'
      - value: 30000
        color: '#731425'
  - type: 'custom:mini-graph-card'
    entities:
      - sensor.purpleair_p_0_5_um_a
    unit: PM 0.5
    name: Ultrafine Particles 0.5 (A)(24hr)
    icon: 'mdi:grain'
    hour24: true
    show:
      fill: true
      legend: false
      labels: false
      state: true
      icon: true
      extrema: true
      average: true
      name: true
      points: false
      name_adaptive_color: true
      icon_adaptive_color: true
      show_legend: false
    font_size: 75
    line_width: 3
    points_per_hour: 4
    hours_to_show: 24
    color_thresholds_transition: hard
    color_thresholds:
      - value: 0
        color: '#68FF43'
      - value: 1000
        color: '#FFFF55'
      - value: 2000
        color: '#EF8533'
      - value: 4000
        color: '#EA3324'
      - value: 8000
        color: '#8C1A4B'
      - value: 16000
        color: '#731425'
  - type: 'custom:mini-graph-card'
    entities:
      - sensor.purpleair_p_1_0_um_a
    unit: PM 1
    name: Ultrafine Particles 1 (A) (24hr)
    icon: 'mdi:grain'
    hour24: true
    show:
      fill: true
      legend: false
      labels: false
      state: true
      icon: true
      extrema: true
      average: true
      name: true
      points: false
      name_adaptive_color: true
      icon_adaptive_color: true
      show_legend: false
    font_size: 75
    line_width: 3
    points_per_hour: 4
    hours_to_show: 24
    color_thresholds_transition: hard
    color_thresholds:
      - value: 0
        color: '#68FF43'
      - value: 100
        color: '#FFFF55'
      - value: 200
        color: '#EF8533'
      - value: 400
        color: '#EA3324'
      - value: 600
        color: '#8C1A4B'
      - value: 800
        color: '#731425'
  - type: 'custom:mini-graph-card'
    entities:
      - sensor.purpleair_p_2_5_um_a
    unit: PM 2.5
    name: Fine Particles 2.5 (A) (24hr)
    icon: 'mdi:grain'
    hour24: true
    show:
      fill: true
      legend: false
      labels: false
      state: true
      icon: true
      extrema: true
      average: true
      name: true
      points: false
      name_adaptive_color: true
      icon_adaptive_color: true
      show_legend: false
    font_size: 75
    line_width: 3
    points_per_hour: 4
    hours_to_show: 24
    color_thresholds_transition: hard
    color_thresholds:
      - value: 0
        color: '#68FF43'
      - value: 8
        color: '#FFFF55'
      - value: 16
        color: '#EF8533'
      - value: 24
        color: '#EA3324'
      - value: 40
        color: '#8C1A4B'
      - value: 60
        color: '#731425'
  - type: 'custom:mini-graph-card'
    entities:
      - sensor.purpleair_p_5_0_um_a
    unit: PM 5
    name: Coarse Particles 5 (A) (24hr)
    icon: 'mdi:grain'
    hour24: true
    show:
      fill: true
      legend: false
      labels: false
      state: true
      icon: true
      extrema: true
      average: true
      name: true
      points: false
      name_adaptive_color: true
      icon_adaptive_color: true
      show_legend: false
    font_size: 75
    line_width: 3
    points_per_hour: 4
    hours_to_show: 24
    color_thresholds_transition: hard
    color_thresholds:
      - value: 0
        color: '#68FF43'
      - value: 4
        color: '#FFFF55'
      - value: 8
        color: '#EF8533'
      - value: 12
        color: '#EA3324'
      - value: 20
        color: '#8C1A4B'
      - value: 30
        color: '#731425'
  - type: 'custom:mini-graph-card'
    entities:
      - sensor.purpleair_p_10_0_um_a
    unit: PM 10
    name: Coarse Particles 10 (A) (24hr)
    icon: 'mdi:grain'
    hour24: true
    show:
      fill: true
      legend: false
      labels: false
      state: true
      icon: true
      extrema: true
      average: true
      name: true
      points: false
      name_adaptive_color: true
      icon_adaptive_color: true
      show_legend: false
    font_size: 75
    line_width: 3
    points_per_hour: 4
    hours_to_show: 24
    color_thresholds_transition: hard
    color_thresholds:
      - value: 0
        color: '#68FF43'
      - value: 2
        color: '#FFFF55'
      - value: 4
        color: '#EF8533'
      - value: 6
        color: '#EA3324'
      - value: 10
        color: '#8C1A4B'
      - value: 15
        color: '#731425'

would this PurpleAir detect cigarettes being smoked within 10feet?

The sensor sees a PM2.5 spike when we use the stove (with vent exhaust hood running) which is 25’ away from it. Here is my data from last night:

As you can see, my spouse cooked a frittata at about 5:30pm. No idea what the slight bump is at 7:30pm.

So – yeah, assuming the wind wasn’t blowing the smoke away, I’d think a cigarette would be easy to see in the data. What would be more challenging is to say definitively “this is a cigarette!” as opposed to the other zillion sources of PM2.5 spikes.

1 Like

Always modifying my views. I have some tweaking to do, and can post the background image once I get it a little more matching on the AQI. I also am not completely happy with the AQI clip-art at the bottom.

Hi all,

I have a Coway airmega and the custom integration provides a number in μg/m3. I have been looking around for a conversion to AQI, but its not really straightforward:
https://forum.airnowtech.org/t/the-aqi-equation/169
https://www.airnow.gov/aqi/aqi-calculator-concentration/

I’m trying to combine multiple sources of AQI (Airnow and my Xiaomi AQI sensor) into one graph and would be great to get my Coway to have the same unit of measurement. Anyone have an idea how to create the calculation template?

Need more info. It is probably safe to assume this is for 2.5 particle size but it could be 10.
There are different AQI scales also. Unfortunately the same acronym has at least four widely used and different scales.

Edit: post 83 above sets it up for you
https://community.home-assistant.io/t/purpleair-air-quality-sensor/146588/83

Oooh I like those gauges. Are these a mod? Can you share your setup for them?

Gauges are the same as posted above (somewhere up there).
I just ‘recently’ added the backgrounds, and still playing with them.
Give me a little bit and I will just upload my backgrounds and the logos I used to github for easier updating.

1 Like

thanks, I looked more into what is exposed in the integration and I guess it does have “air quality” and PM2.5. However, Im confused with all the units:

Airnow AQI:
image

Airnow PM2.5:
image

Airmega 400S:
image

Xiaomi AQI monitor:
image

I currently have the Airnow AQI, Airmega Air Quality Index, and Xiaomi in one graph but its not really making sense? The Airmega shows ug/m3 as the units for AQI. In the PM2.5 graph, Airnow has ug/m3 as units. I think I may the Airmega sensors swapped in the graphs?

https://github.com/GlennGoddard/CanvasGaugeBackgrounds

Still a work in progress.
I have each gauge in a directory.
I have the front-end yaml code, background, and any logo used.

I have shifted from embedding the text in the background to using state-label to allow anyone to change the text to the language of their choice.

I don’t have every thing uploaded yet, since I don’t have all the images on me at this time.

You will just have to modify the yaml to fit whatever sensors you have.

If anyone is interested I successfully created a REST sensor that calculates the Canadian AQHI from the current version of the Purple Air API.
https://api.purpleair.com/#api-sensors-get-sensor-data

You will need to replace the following with your own values:

  • <sensor_index>
    The id of the Purple Air sensor you want to monitor. You can get this a few places including in the interactive Purple Air map by hovering over the Get This Widget link at the bottom of the pop-up when you hover over one of the sensors. You’ll see id='PurpleAirWidget_1234_module where 1234 is the sensor id

  • purple_air_api_key
    your purple air API key added to your secrets file

  • pm2.5_10minute
    Choose whatever average you want. My preference is the 10 minute average. I think simply pm2.5 will give you the real-time reading.

  - platform: rest
    name: localized_aqhi
    resource: "https://api.purpleair.com/v1/sensors/<sensor_index>?fields=pm2.5_10minute"
    headers:
      X-API-Key: !secret purple_air_api_key
    method: GET
    unit_of_measurement: "AQHI"
    scan_interval: 600
    value_template: "{{ ((10/10.4) * 100 * (2.71828**(0.000487 * value_json['sensor']['stats']['pm2.5_10minute'])-1)) | round (0) }}"

Unfortunately the Purple Air sensors (at least the ones in my area?) don’t return NO2 and O3 which are technically part of the AQHI calculation so this is more so a raw PM2.5 reading converted to the AQHI scale than a true AQHI reading, but it should be relatively close.
https://www.tandfonline.com/doi/pdf/10.3155/1047-3289.58.3.435?needAccess=true

I’m mostly interested in these readings because of the forest fires in our area which typically have a much larger impact on our air quality (and our AQHI reading) than NO2 and O3 so probably not a huge deal. The official Environment Canada reading for our city is a fair distance away and at a much lower elevation and is often much different than the closer by Purple Air sensors since we have a lot of wind and different wind / weather patterns where we are. Plus Environment Canada readings only update once every hour and conditions can change quickly with the fire smoke.

Non-Canadians can use this for the raw PM2.5 reading as well by trimming down the value_template to just this:

value_template: "value_json['sensor']['stats']['pm2.5_10minute']"
1 Like

This is great, thank you. I was looking for some code to pull data from purple air using the API key. Can you share the code you use to convert that PM2.5 reading to an AQI number?

Also - Have you looked at showing data from multiple purple air sensors?

-Bill_Automated

I am now getting this error when I try and load the Purpleair integration. Thoughts on how to fix this?

2021-08-13 11:38:38 ERROR (SyncWorker_2) [homeassistant.loader] The custom integration ‘purpleair’ does not have a valid version key (None) in the manifest file and was blocked from loading. See Custom integration changes | Home Assistant Developer Docs for more details

Thanks,

-Bill_Automated

The formula to convert to the Canadian AQHI is included but that’s all I’ve done since I live in Canada and that’s all I care about. You would have to look up the AQI formula and just replace what I have as needed.

The API doc shows how to get multiple sensors at once and I plan to add that eventually but haven’t done that yet. Likely it would just be a matter adjusting the API call according to the doc and then pulling the results from the multiple sensors via an index, something like ‘value_json[0][‘sensor’][‘stats’]…’, ‘value_json[1][‘sensor’][‘stats’]…’, etc but don’t quote me. I’m on mobile and can’t look at the API doc right now. Then you’d just have to average that out or do whatever else you want with the individual readings.