PurpleAir Air Quality Sensor

Yes, it does. You can see all the data it returns by looking at the PurpleAir map, here is a random sensor site I just clicked on.

I get my data from the PurpleAir sensor locally - so I’m not hitting their API, and so I keep getting data if there are internet / API issues.
The data is available at http://n.n.n.n/json?live=true where n.n.n.n is your sensors IP address.

3 Likes

Huh. Indeed it does. I unknowingly had the subnet of my home network this was on configured to forbid connections between hosts on the network – which means I couldn’t connect to it. And mistook that for “guess that doesn’t work”.

I’ve fixed my firewall rules, and now I can get at this locally. Thanks!

1 Like

Hi…thanks a lot for sharing this. I’ll definitely incorporate to my HA. I intend to add it in my personal website. You said you convert it from your javascript. Would you mind sharing your javascript on this part?

It’s not my javascript – it is PurpleAir’s javascript. The link to the original code is provided in my code: https://docs.google.com/document/d/15ijz94dXJ-YAZLi9iZ_RaBwrZ4KtYeCy08goGBwnbCU/edit#

Thanks a lot, I just saw it in your HA config. Sorry, I’m quite new to Javascript. Is it a complete script starting from pulling data from PurpleAir till populating the desired values to variables that I can pick from?
Thanks.

@colohan - Thanks for sharing your purple air implementation. I grabbed that plus the excellent gauge card (https://github.com/custom-cards/canvas-gauge-card) and mapped the values to the colours on the AQI charts.

aqi_gauge

      - type: custom:canvas-gauge-card
        card_height: 200
        entity: sensor.purpleair
        name: AQI reading
        style:
          top: 50%
          left: 50%
        gauge:
          type: "radial-gauge"
          title: AQI
          width: 200
          height: 200
          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: 2
          valueInt: 2
      - type: entities
        title: My Area 
        show_header_toggle: false
        entities:
          - entity: sensor.purpleair_description
            name: Air Quality Index (AQI)
          - entity: sensor.purpleair_pm25
            name: PurpleAir PM 2.5
          - entity: sensor.purpleair_temperature
            name: PurpleAir Temperature
          - entity: sensor.purpleair_humidity
            name: PurpleAir Humidity
          - entity: sensor.purpleair_pressure
            name: PurpleAir Pressure
4 Likes

@bthoven – If you are using Home Assistant, the Javascript implementation is not relevant to you – it is simply where I learned how to create this Home Assistant configuration. The above script is written in YAML (with some embedded Jinja2 scripting, since Home Assistant configs use both (which sadly makes it much harder to learn…)).

If you cut-and-paste the code above into your configuration.yaml, then you will end up with a series of new variables defined in your Home Assistant (such as sensor.purpleair_aqi, sensor.purpleair_humidity, etc.) that you can either use as inputs to widgets, or use in your own automations. These will tell you all about the air quality in my home. If you modify the resource: line you can have it tell you about the air quality in your home instead. :slight_smile:

@colohan Thanks. I do not own a sensor. Apart from HA, I also have a website designed just for a weather station console, thanks to a guy who originated and shared it. The latter was driven mainly by html, javascript and css. I’m not sure how the result from configuration.yaml used in HA can be somehow reused for my website.

Btw, just did it with a simple Javascript for my website. Thanks to the PurpleAir document you mentioned.

The local API seems to be slightly different. Here is my config:

sensor:
  - platform: rest
    name: 'PurpleAir'
    resource: http://<myhost>/json?live=true
    value_template: '{{ value_json.SensorId }}'
    json_attributes:
      - Mem
      - memfrag
      - uptime
      - rssi
      - Adc
      - current_temp_f
      - current_humidity
      - current_dewpoint_f
      - pressure
      - p_0_3_um
      - p_0_3_um_b
      - p_0_5_um
      - p_0_5_um_b
      - p_1_0_um
      - p_1_0_um_b
      - pm1_0_atm
      - pm1_0_atm_b
      - p_2_5_um
      - p_2_5_um_b
      - pm2_5_atm
      - pm2_5_atm_b
      - p_5_0_um
      - p_5_0_um_b
      - p_10_0_um
      - p_10_0_um_b
      - pm10_0_atm
      - pm10_0_atm_b
      - pm2.5_aqi
      - pm2.5_aqi_b
  - platform: template
    sensors:
      purpleair_memory:
        friendly_name: "PurpleAir Memory"
        value_template: '{{ states.sensor.purpleair.attributes["Mem"] }}'
      purpleair_adc:
        friendly_name: "PurpleAir ADC"
        value_template: '{{ states.sensor.purpleair.attributes["Adc"] }}'
      purpleair_uptime:
        friendly_name: "PurpleAir Uptime"
        value_template: '{{ states.sensor.purpleair.attributes["uptime"] }}'
        unit_of_measurement: "seconds"
      purpleair_memory_fragmentation:
        friendly_name: "PurpleAir Memory Fragmentation"
        value_template: '{{ states.sensor.purpleair.attributes["memfrag"] }}'
      purpleair_wifi_rssi:
        friendly_name: "PurpleAir Wifi RSSI"
        value_template: '{{ states.sensor.purpleair.attributes["rssi"] }}'
      purpleair_temp:
        friendly_name: "PurpleAir Temperature"
        value_template: '{{ states.sensor.purpleair.attributes["current_temp_f"] }}'
        unit_of_measurement: "°F"
      purpleair_humidity:
        friendly_name: "PurpleAir Humidity"
        value_template: '{{ states.sensor.purpleair.attributes["current_humidity"] }}'
        unit_of_measurement: "%"
      purpleair_dewpoint:
        friendly_name: "PurpleAir Dewpoint"
        value_template: '{{ states.sensor.purpleair.attributes["current_dewpoint_f"] }}'
        unit_of_measurement: "°F"
      purpleair_pressure:
        friendly_name: "PurpleAir Pressure"
        value_template: '{{ states.sensor.purpleair.attributes["pressure"] }}'
        unit_of_measurement: "mbar"
      purpleair_aqi_a:
        friendly_name: "PurpleAir AirQuality A"
        value_template: '{{ states.sensor.purpleair.attributes["pm2.5_aqi"] }}'
        unit_of_measurement: "AQI"
      purpleair_aqi_b:
        friendly_name: "PurpleAir AirQuality B"
        value_template: '{{ states.sensor.purpleair.attributes["pm2.5_aqi_b"] }}'
        unit_of_measurement: "AQI"
      purpleair_p_0_3_um_a:
        friendly_name: "PurpleAir .3um Partical Count A"
        value_template: '{{ states.sensor.purpleair.attributes["p_0_3_um"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_0_3_um_b:
        friendly_name: "PurpleAir .3um Partical Count B"
        value_template: '{{ states.sensor.purpleair.attributes["p_0_3_um_b"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_0_5_um_a:
        friendly_name: "PurpleAir .5um Partical Count A"
        value_template: '{{ states.sensor.purpleair.attributes["p_0_5_um"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_0_5_um_b:
        friendly_name: "PurpleAir .5um Partical Count B"
        value_template: '{{ states.sensor.purpleair.attributes["p_0_5_um_b"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_1_0_um_a:
        friendly_name: "PurpleAir 1.0um Partical Count A"
        value_template: '{{ states.sensor.purpleair.attributes["p_1_0_um"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_1_0_um_b:
        friendly_name: "PurpleAir 1.0um Partical Count B"
        value_template: '{{ states.sensor.purpleair.attributes["p_1_0_um_b"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_2_5_um_a:
        friendly_name: "PurpleAir 2.5um Partical Count A"
        value_template: '{{ states.sensor.purpleair.attributes["p_2_5_um"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_2_5_um_b:
        friendly_name: "PurpleAir 2.5um Partical Count B"
        value_template: '{{ states.sensor.purpleair.attributes["p_2_5_um_b"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_5_0_um_a:
        friendly_name: "PurpleAir 5.0um Partical Count A"
        value_template: '{{ states.sensor.purpleair.attributes["p_5_0_um"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_5_0_um_b:
        friendly_name: "PurpleAir 5.0um Partical Count B"
        value_template: '{{ states.sensor.purpleair.attributes["p_5_0_um_b"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_10_0_um_a:
        friendly_name: "PurpleAir 10.0um Partical Count A"
        value_template: '{{ states.sensor.purpleair.attributes["p_10_0_um"] }}'
        unit_of_measurement: "um/dl"
      purpleair_p_10_0_um_b:
        friendly_name: "PurpleAir 10.0um Partical Count B"
        value_template: '{{ states.sensor.purpleair.attributes["p_10_0_um_b"] }}'
        unit_of_measurement: "um/dl"
      purpleair_pm10_atm_a:
        friendly_name: "PurpleAir 10.0um Mass A"
        value_template: '{{ states.sensor.purpleair.attributes["pm10_0_atm"] }}'
        unit_of_measurement: "ug/m3"
      purpleair_pm10_atm_b:
        friendly_name: "PurpleAir 10.0um Mass B"
        value_template: '{{ states.sensor.purpleair.attributes["pm10_0_atm_b"] }}'
        unit_of_measurement: "ug/m3"
      purpleair_pm1_atm_a:
        friendly_name: "PurpleAir 1.0um Mass A"
        value_template: '{{ states.sensor.purpleair.attributes["pm1_0_atm"] }}'
        unit_of_measurement: "ug/m3"
      purpleair_pm1_atm_b:
        friendly_name: "PurpleAir 1.0um Mass B"
        value_template: '{{ states.sensor.purpleair.attributes["pm1_0_atm_b"] }}'
        unit_of_measurement: "ug/m3"
      purpleair_pm2_5_atm_a:
        friendly_name: "PurpleAir 2.5um Mass A"
        value_template: '{{ states.sensor.purpleair.attributes["pm2_5_atm"] }}'
        unit_of_measurement: "ug/m3"
      purpleair_pm2_5_atm_b:
        friendly_name: "PurpleAir 2.5um Mass B"
        value_template: '{{ states.sensor.purpleair.attributes["pm2_5_atm_b"] }}'
        unit_of_measurement: "ug/m3"
7 Likes

I did something similar, but use Node Red running against the local API:

[{"id":"7a752971.c61c08","type":"tab","label":"Purple Air","disabled":false,"info":""},{"id":"fa0d0904.ff7658","type":"inject","z":"7a752971.c61c08","name":"Poll every minute","topic":"","payload":"","payloadType":"date","repeat":"60","crontab":"","once":true,"onceDelay":0.1,"x":141,"y":406,"wires":[["5d3ed5bb.302cbc"]]},{"id":"5d3ed5bb.302cbc","type":"http request","z":"7a752971.c61c08","name":"Get measurements","method":"GET","ret":"obj","paytoqs":false,"url":"http://192.168.2.153/json?live=true","tls":"","persist":false,"proxy":"","authType":"","x":410,"y":406,"wires":[["d04a0995.0eafc8","527d4cd7.c639a4","e9bb02fd.ec1f6","a4e7f11b.763d4"]]},{"id":"d04a0995.0eafc8","type":"unit-converter","z":"7a752971.c61c08","category":"temperature","inputUnit":"F","outputUnit":"C","inputField":"payload.current_temp_f","outputField":"payload","inputFieldType":"msg","outputFieldType":"msg","name":"F to C","x":630,"y":280,"wires":[["b4006d9a.103d8"]]},{"id":"e9bb02fd.ec1f6","type":"ha-entity","z":"7a752971.c61c08","name":"Atmospheric Pressure","server":"421dff97.2edf9","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"Purple Air Pressure"},{"property":"device_class","value":"pressure"},{"property":"icon","value":""},{"property":"unit_of_measurement","value":"hPa"}],"state":"payload.pressure","stateType":"msg","attributes":[],"resend":true,"outputLocation":"","outputLocationType":"none","inputOverride":"allow","x":1080,"y":404,"wires":[[]]},{"id":"527d4cd7.c639a4","type":"unit-converter","z":"7a752971.c61c08","category":"temperature","inputUnit":"F","outputUnit":"C","inputField":"payload.current_dewpoint_f","outputField":"payload","inputFieldType":"msg","outputFieldType":"msg","name":"F to C","x":630,"y":340,"wires":[["9d6a1a8c.fb73d8"]]},{"id":"17c43b58.ec8cc5","type":"ha-entity","z":"7a752971.c61c08","name":"Outside Temperature","server":"421dff97.2edf9","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"Purple Air Temperature"},{"property":"device_class","value":"temperature"},{"property":"icon","value":""},{"property":"unit_of_measurement","value":"°C"}],"state":"payload","stateType":"msg","attributes":[],"resend":true,"outputLocation":"","outputLocationType":"none","inputOverride":"allow","x":1080,"y":280,"wires":[[]]},{"id":"6f267455.cf1b8c","type":"ha-entity","z":"7a752971.c61c08","name":"Dewpoint","server":"421dff97.2edf9","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"Purple Air Dewpoint"},{"property":"device_class","value":"temperature"},{"property":"icon","value":""},{"property":"unit_of_measurement","value":"°C"}],"state":"payload","stateType":"msg","attributes":[],"resend":true,"outputLocation":"","outputLocationType":"none","inputOverride":"allow","x":1040,"y":340,"wires":[[]]},{"id":"b4006d9a.103d8","type":"function","z":"7a752971.c61c08","name":"Single Decimal Place","func":"var x = Number(msg.payload)\n\nmsg.payload = x.toFixed(1)\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":280,"wires":[["17c43b58.ec8cc5"]]},{"id":"9d6a1a8c.fb73d8","type":"function","z":"7a752971.c61c08","name":"Single Decimal Place","func":"var x = Number(msg.payload)\n\nmsg.payload = x.toFixed(1)\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":340,"wires":[["6f267455.cf1b8c"]]},{"id":"a4e7f11b.763d4","type":"function","z":"7a752971.c61c08","name":"PM 2.5 AQI","func":"var x = Number(msg.payload[\"pm2.5_aqi\"])\nmsg.payload = x.toFixed(0)\nreturn msg;","outputs":1,"noerr":0,"x":650,"y":460,"wires":[["e5fb9dd8.358ad"]]},{"id":"ad11ab29.8ba6f8","type":"ha-entity","z":"7a752971.c61c08","name":"AQI 2.5","server":"421dff97.2edf9","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"Purple Air AQI 2.5"},{"property":"device_class","value":""},{"property":"icon","value":""},{"property":"unit_of_measurement","value":"AQI"}],"state":"payload","stateType":"msg","attributes":[],"resend":true,"outputLocation":"","outputLocationType":"none","inputOverride":"allow","x":1040,"y":460,"wires":[[]]},{"id":"e5fb9dd8.358ad","type":"function","z":"7a752971.c61c08","name":"Moving Average","func":"// determines the average of all payload values passed in \n// over the specified time range (10 minutes)\nconst range = 10 * 60 * 1000;   // window time millisecs\nlet buffer = context.get('buffer') || [];\nlet total = context.get('total') || 0;   // the accumulated total so far\n\nlet now = new Date();\nlet value = Number(msg.payload);\n// remove any samples that are too old\nwhile (buffer[0] && buffer[0].timestamp < now - range) {\n    // remove oldest sample from array and total\n    node.warn(`removing oldest ${buffer[0].timestamp}`);\n    total -= buffer[0].value;\n    buffer.shift();\n}\n// add the new sample to the end\nbuffer.push({timestamp: now, value: value});\ntotal += value;\n\ncontext.set('buffer', buffer);\ncontext.set('total', total);\n\nmsg.payload = (total/buffer.length).toFixed(0);\n// uncommnet the following to see the buffer status in real time\n//node.warn(`length: ${buffer.length}, total: ${total}, average: ${msg.payload}`);\nreturn msg;","outputs":1,"noerr":0,"x":840,"y":460,"wires":[["ad11ab29.8ba6f8"]]},{"id":"421dff97.2edf9","type":"server","z":"","name":"Home Assistant","legacy":false,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

I used the Node Red Companion Component for node-red-contrib-home-assistant-websocket from HACS to make creating the sensors easy.

2 Likes

I have two sensors, one indoors and outdoors, and each sensor has a primary and secondary sensor.
The script uses just the primary, some discussion suggests to average the pri and sec sensors, or display both, like the map does.

Is there an easy way to convert the rest code to an easier integration, i.e. without copy and pasting the code 4 times?

Thanks for this recipe! It’s pretty good!

I noticed the temperature seemed inaccurate, and I noticed that purpleair website seems to do a transformation on the temperature, it’s noted in deep in the FAQ - https://www2.purpleair.com/community/faq#!hc-primary-and-secondary-data-header - you should subtract 8 from the temp_f, so if the json shows e.g. 76, purpleair shows 68, and the temperature outside is probably closer to 68 than 76.

I see github activity around the air quality sensors, including built in transformations functions.

How do I use the HA air_quality platform?
E.g. pass in PM2.5 values and AQI without needing to use the calcAQI macro?
E.g. get an air quality UI widget?

Oooh, good find @dango! I’ve always assumed that either my temperature sensor was defective, or just useless due to it getting heated by the PurpleAir computer itself. I’ll try adding in that offset in my config and see if it gives me more reasonable data…

@ptr727: I honestly don’t know, as I’ve not tried the air_quality platform. If you figure it out, please be sure to let us know. :slight_smile:

With the entire west US pretty much on fire at this point, I was very much interested in a PurpleAir integration. Thanks @colohan for the groundwork on the REST template. After playing around a bit, I ended up making a very quick and dirty custom component to better integrate and get all the sensor goodness that comes with it, as I was interested in some of the raw parts of the data outside the AQI.

This is my first component, but it’s worked pretty well in my use case so far today. You can grab the source at https://gitlab.com/gibwar/home-assistant-purpleair and add it to your custom components. It creates a proper air_quality sensor with the correct PM 1.0, PM 2.5, and PM 10 data averaged out if the sensor you pick has an A and B channel. It only supports setup via the GUI and only with sensors that have free public access and it’ll batch the updates every 5 minutes for any additional sensors you add. It also only supports the web API, as I don’t have a device locally to test/setup a local API. I don’t think it’d be terribly difficult to add though. It’d need to flag it and update it outside the update schedule as that batches the sensor IDs together to lessen the load on their servers.

As I said, this is my first integration, so I hope you all find it useful.

7 Likes

Thanks for the excellent configuration, @colohan. I made some small tweaks for my own use that others might find useful. Namely, I made the value of the data holder the last time the sensor was seen, moved the AQI value to a template sensor, added in the 8 degree temperature adjustment as suggested, put a couple variables in the template to reduce repetition in the conditionals so they’re easier to read, and added unique_ids and device_classes to the entities, and added a binary_sensor for connectivity and use in availability_template values.

  # https://community.home-assistant.io/t/purpleair-air-quality-sensor/146588
  # Reasonable sensors pulled from https://www.purpleair.com/map: 37021, 20801, 20707.
  - platform: rest
    name: 'PurpleAir'

    # Substitute in the URL of the sensor you care about. To find the URL, go
    # to purpleair.com/map, find your sensor, click on it, click on "Get This
    # Widget" then click on "JSON".
    resource: https://www.purpleair.com/json?key=PMWGYB07N4CDBH23&show=37021

    # Only query once a minute to avoid rate limits:
    scan_interval: 60

    # Set the sensor value to the last update time of the device.
    device_class: timestamp
    value_template: >
      {{ state_attr('sensor.purpleair', 'results')[0]['LastSeen'] }}

    # The value of the sensor can't be longer than 255 characters, but the
    # attributes can. Store away all the data for use by the templates below.
    json_attributes:
      - results

  - platform: template
    sensors:
      purpleair_aqi:
        friendly_name: 'PurpleAir AQI'
        unique_id: purpleair_aqi
        # Set this sensor to be the AQI value.
        #
        # Code translated from JavaScript found at:
        # https://docs.google.com/document/d/15ijz94dXJ-YAZLi9iZ_RaBwrZ4KtYeCy08goGBwnbCU/edit#
        value_template: >
          {% macro calcAQI(Cp, Ih, Il, BPh, BPl) -%}
            {{ (((Ih - Il)/(BPh - BPl)) * (Cp - BPl) + Il)|round }}
          {%- endmacro %}
          {% set pm25 = state_attr('sensor.purpleair', 'results')[0]['PM2_5Value']|float %}
          {% if pm25 > 1000 %}
            invalid
          {% elif pm25 > 350.5 %}
            {{ calcAQI(pm25, 500.0, 401.0, 500.0, 350.5) }}
          {% elif pm25 > 250.5 %}
            {{ calcAQI(pm25, 400.0, 301.0, 350.4, 250.5) }}
          {% elif pm25 > 150.5 %}
            {{ calcAQI(pm25, 300.0, 201.0, 250.4, 150.5) }}
          {% elif pm25 > 55.5 %}
            {{ calcAQI(pm25, 200.0, 151.0, 150.4, 55.5) }}
          {% elif pm25 > 35.5 %}
            {{ calcAQI(pm25, 150.0, 101.0, 55.4, 35.5) }}
          {% elif pm25 > 12.1 %}
            {{ calcAQI(pm25, 100.0, 51.0, 35.4, 12.1) }}
          {% elif pm25 >= 0.0 %}
            {{ calcAQI(pm25, 50.0, 0.0, 12.0, 0.0) }}
          {% else %}
            invalid
          {% endif %}
        unit_of_measurement: "AQI"
        entity_id: sensor.purpleair
        availability_template: "{{ sensor.purpleair_available }}"
      purpleair_description:
        friendly_name: 'PurpleAir AQI Description'
        unique_id: purpleair_description
        value_template: >
          {% set aqi = states('sensor.purpleair_aqi')|float %}
          {% if aqi >= 401.0 %}
            Very Hazardous
          {% elif aqi >= 301.0 %}
            Hazardous
          {% elif aqi >= 201.0 %}
            Very Unhealthy
          {% elif aqi >= 151.0 %}
            Unhealthy
          {% elif aqi >= 101.0 %}
            Unhealthy for Sensitive Groups
          {% elif aqi >= 51.0 %}
            Moderate
          {% elif aqi >= 0.0 %}
            Good
          {% else %}
            undefined
          {% endif %}
        entity_id: sensor.purpleair
        availability_template: "{{ sensor.purpleair_available }}"
      purpleair_pm25:
        friendly_name: 'PurpleAir PM 2.5'
        unique_id: purpleair_pm25
        value_template: "{{ state_attr('sensor.purpleair', 'results')[0]['PM2_5Value'] }}"
        unit_of_measurement: "μg/m3"
        entity_id: sensor.purpleair
        availability_template: "{{ sensor.purpleair_available }}"
      purpleair_temp:
        friendly_name: 'PurpleAir Temperature'
        unique_id: purpleair_temp
        # Why "- 8"?
        # https://www2.purpleair.com/community/faq#!hc-primary-and-secondary-data-header
        value_template: "{{ state_attr('sensor.purpleair', 'results')[0]['temp_f'] | float - 8 }}"
        unit_of_measurement: "°F"
        device_class: temperature
        entity_id: sensor.purpleair
        availability_template: "{{ sensor.purpleair_available }}"
      purpleair_humidity:
        friendly_name: 'PurpleAir Humidity'
        unique_id: purpleair_humidity
        value_template: "{{ state_attr('sensor.purpleair', 'results')[0]['humidity'] }}"
        unit_of_measurement: "%"
        device_class: humidity
        entity_id: sensor.purpleair
        availability_template: "{{ sensor.purpleair_available }}"
      purpleair_pressure:
        friendly_name: 'PurpleAir Pressure'
        unique_id: purpleair_pressure
        value_template: "{{ state_attr('sensor.purpleair', 'results')[0]['pressure'] }}"
        unit_of_measurement: "mb"
        device_class: pressure
        entity_id: sensor.purpleair
        availability_template: "{{ sensor.purpleair_available }}"


binary_sensor:
  - platform: template
    sensors:
      # Add a connectivity sensor for purpleair. Why isn't the above rest sensor
      # a binary_sensor? Turns out binary_sensor rest devices can't have a
      # json_attributes section.
      purpleair_available:
        friendly_name: 'PurpleAir Available'
        unique_id: purpleair_available
        value_template: > 
          {% set stats = state_attr('sensor.purpleair', 'results')[0]['Stats']|from_json %}
          {% set minutes = stats['timeSinceModified']|int / 1000 /60 %}
          {{ minutes < 5 }}
        entity_id: sensor.purpleair
        device_class: connectivity
4 Likes

@michael.olson thanks for the tweaks! Due to wildfires nearby, this data is very essential for us.
How do you and/or others display this data? I’m newbie to HA and wanted to know if I am doing it right.
Here’s how I display my entities:

I’ve not incorporated some of the excellent tweaks from others above (yet) into my config, but I’m also located in the Bay Area, so smoke is all I care about right now. I figured that what I care about is:

  1. how bad is it outside; and
  2. can I safely cool my house by opening a window right now?

To answer those two simple questions, what I have on my home screen is this:
image

It simply shows me the temperature inside (from my PurpleAir), outside (from a Multisensor 6), AQI inside (from my PurpleAir), and outside (averages the 3 closest outdoor PurpleAir devices to my home). Clicking on any of those shows a history graph, which can be helpful.

The code I have for the averaging is this:

  - platform: template
    sensors:
      purpleair_outdoor_aqi:
        friendly_name: 'Outdoor AQI from neighbours'
        # Average three sensors so if one goes down I still have data:
        value_template: >-
          {% set sum = 0.0 %}
          {% set count = 0.0 %}
          {% if states('sensor.purpleair_aqi_neighbour_1') != 'invalid' %}
            {% set sum = sum + states('sensor.purpleair_aqi_neighbour_1')|float %}
            {% set count = count + 1 %}
          {% endif %}
          {% if states('sensor.purpleair_aqi_neighbour_2') != 'invalid' %}
            {% set sum = sum + states('sensor.purpleair_aqi_neighbour_2')|float %}
            {% set count = count + 1 %}
          {% endif %}
          {% if states('sensor.purpleair_aqi_neighbour_3') != 'invalid' %}
            {% set sum = sum + states('sensor.purpleair_aqi_neighbour_3')|float %}
            {% set count = count + 1 %}
          {% endif %}
          {{ (sum / count)|int }}
        unit_of_measurement: "AQI"
2 Likes