Not sure if this is really a “custom component”, but I thought that other people might find this useful. I recently got a Purple Air outdoor sensor and wanted to pull the data into Home Assistant. When I ran out of tokens for the Purple Air API, I almost reached out about getting additional tokens, but instead decided to look into the custom “data processor” configuration.
I found a copy of what the data processor payload looks like on the Purple Air Discourse instance and used that to write the following configuration:
template:
- unique_id: Purple Air
trigger:
- platform: webhook
webhook_id: !secret purple_air_webhook_id
allowed_methods: [POST]
local_only: true
# List of sensors taken from Purple Air integration
# Sample payload at https://community.purpleair.com/t/custom-data-processor-schema/2214
sensor:
- name: Purple Air Temperature
unique_id: Temperature
device_class: temperature
unit_of_measurement: °F
state_class: measurement
state: "{{ trigger.json.current_temp_f }}"
- name: Purple Air Pressure
unique_id: Pressure
device_class: pressure
unit_of_measurement: mbar
state_class: measurement
state: "{{ trigger.json.pressure }}"
- name: Purple Air Humidity
device_class: humidity
unit_of_measurement: "%"
state_class: measurement
state: "{{ trigger.json.current_humidity }}"
- name: Purple Air PM0.3 count concentration
icon: mdi:blur
unit_of_measurement: particles/100mL
state_class: measurement
state: "{{ (trigger.json.p_0_3_um + trigger.json.p_0_3_um_b) / 2 }}"
- name: Purple Air PM0.5 count concentration
icon: mdi:blur
unit_of_measurement: particles/100mL
state_class: measurement
state: "{{ (trigger.json.p_0_5_um + trigger.json.p_0_5_um_b) / 2 }}"
- name: Purple Air PM1.0 count concentration
icon: mdi:blur
unit_of_measurement: particles/100mL
state_class: measurement
state: "{{ (trigger.json.p_1_0_um + trigger.json.p_1_0_um_b) / 2 }}"
- name: Purple Air PM2.5 count concentration
icon: mdi:blur
unit_of_measurement: particles/100mL
state_class: measurement
state: "{{ (trigger.json.p_2_5_um + trigger.json.p_2_5_um_b) / 2 }}"
- name: Purple Air PM5.0 count concentration
icon: mdi:blur
unit_of_measurement: particles/100mL
state_class: measurement
state: "{{ (trigger.json.p_5_0_um + trigger.json.p_5_0_um_b) / 2 }}"
- name: Purple Air PM1.0 count concentration
icon: mdi:blur
unit_of_measurement: particles/100mL
state_class: measurement
state: "{{ (trigger.json.p_10_0_um + trigger.json.p_10_0_um_b) / 2 }}"
- name: Purple Air PM1.0 mass concentration
device_class: pm1
unit_of_measurement: µg/m³
state_class: measurement
state: "{{ (trigger.json.pm1_0_atm + trigger.json.pm1_0_atm_b) / 2 }}"
- name: Purple Air PM2.5 mass concentration
device_class: pm25
unit_of_measurement: µg/m³
state_class: measurement
state: "{{ (trigger.json.pm2_5_atm + trigger.json.pm2_5_atm_b) / 2 }}"
- name: Purple Air PM10 mass concentration
device_class: pm10
unit_of_measurement: µg/m³
state_class: measurement
state: "{{ (trigger.json.pm10_0_atm + trigger.json.pm10_0_atm_b) / 2 }}"
- name: Purple Air RSSI
device_class: signal_strength
unit_of_measurement: dBm
state: "{{ trigger.json.rssi }}"
- name: Purple Air Uptime
device_class: duration
unit_of_measurement: s
state_class: total_increasing
state: "{{ trigger.json.uptime }}"
- name: Purple Air AQI
device_class: aqi
state_class: measurement
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 = (trigger.json.pm2_5_atm + trigger.json.pm2_5_atm_b) / 2 %}
{% 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 %}
500
{% endif %}
It’s not exactly the same as what I’d get back from the official API, but it’s pretty close. And because the Purple Air sensor is sending the requests (rather than some centralized service), I don’t have to rely on cloud access to get up to date data.
And for some bonus configuration, here’s a Lovelace gauge configuration to show it, complete with the EPA AQI ranges:
type: gauge
entity: sensor.purple_air_aqi
name: Outdoor AQI
max: 500
needle: true
segments:
- from: 0
color: var(--success-color)
- from: 51
color: yellow
- from: 101
color: orange
- from: 151
color: var(--error-color)
- from: 201
color: purple
- from: 301
color: maroon