Releases: jpettitt/purpleair-local
Release list
v0.1.1 — AQI category & color attributes (US/EU/UK schemes)
Minor release: AQI category + colour as entity attributes.
Every AQI entity now carries two extra attributes that any
template-aware dashboard card can read — making "icon turns green
when air is clean, red when it's not" a one-line card config.
What's new
category— a stable snake-case label for the current AQI
band (good,moderate,unhealthy_for_sensitive_groups, …).category_color— the official hex colour for that band.aqi_color_schemeoption in the options flow with three
country presets:- US EPA (default) — AirNow 2024-revised PM2.5 palette
- EU EAQI — European Environment Agency bands & colours
- UK DAQI — Defra Daily Air Quality Index, 10 numeric bands
The scheme controls the category and category_color attributes
only — the AQI integer state stays on the post-2024 US-EPA
breakpoint table for consistency across schemes.
Example: Mushroom card
type: custom:mushroom-template-card
entity: sensor.outdoor_0119_aqi_epa
icon: mdi:weather-hazy
icon_color: |
{{ state_attr('sensor.outdoor_0119_aqi_epa', 'category_color') }}
primary: |
{{ states('sensor.outdoor_0119_aqi_epa') }} AQI
secondary: |
{{ state_attr('sensor.outdoor_0119_aqi_epa', 'category')
| replace('_', ' ') | title }}See README → Dashboard examples
for apexcharts-card and button-card snippets.
Internals worth knowing
- The category + colour both derive from the same intermediate
µg/m³ value the AQI state is computed from, so colour and number
can't drift. Raw entities use ATM; EPA/AQandU/LRAPA use their
corrected value. - Adding more national schemes later is a tuple literal in
aqi.py— the entity layer is scheme-agnostic.
Upgrading from v0.1.0
No breaking changes. Existing config entries keep their settings
and pick up the default US EPA scheme automatically. New options
flow field appears the next time you open Configure.
Full changelog: v0.1.0...v0.1.1
v0.1.0 — initial release
PurpleAir Local v0.1.0 — initial release.
A Home Assistant custom integration that polls PurpleAir PA-II (and compatible) sensors directly on the LAN. No cloud, no API key.
What's new
Local polling
- Async aiohttp client around the sensor's
/jsonendpoint with a 10 s default timeout and a single retry on transient errors. Defaults to 120 s polling cadence (the sensor's natural averaging window), configurable to 15–3600 s in the options flow.
PM mass entities (µg/m³, ATM density)
PM1.0,PM2.5,PM10— primary on every sensor; per-channel A and B variants added on dual-laser units.
PM2.5 AQI entities — one per enabled correction, using the 2024-revised US EPA breakpoint table (AQI 50 = 9.0 µg/m³, AQI 500 = 325.4 µg/m³, with extrapolation above 500 so wildfire-era readings still produce a meaningful number):
- Raw — uncorrected ATM density. Computed the same way for primary and per-channel entities so they line up at identical inputs (the on-device
pm2.5_aqifield is not passed through, because it uses the pre-2024 EPA breakpoints). - EPA (Barkjohn 2021) —
0.524 × pm_cf1 − 0.0862 × RH + 5.75. Requires the sensor's BME for humidity. - AQandU (optional) —
0.778 × pm_cf1 + 2.65. - LRAPA (optional) —
0.5 × pm_cf1 − 0.66, wood-smoke tuned.
Defaults: Raw + EPA. Toggle others via the options flow.
Channel-disagreement handling (dual-laser units only)
- A
channel_disagreementbinary sensor trips when both PurpleAir thresholds are crossed:|A − B| ≥ 5 µg/m³and|A − B| / max(A, B) ≥ 70 %. Thresholds adjustable in the options flow. - When disagreement trips, the primary PM mass, AQI, and particle-count entities fall back to the lower of the two channel values rather than averaging (the canonical PA failure mode is laser degradation reading high; lower is the conservative pick). The per-channel A and B entities are unaffected so you can see exactly which laser is out of step.
Environment entities (only when a BME280 or BME680 is detected) — temperature, humidity, dewpoint, pressure. VOC resistance entity added when a BME680 is present. BME680 values are preferred when both sensors are present.
⚠️ The reported temperature runs a few degrees high — the BME sits inside the PurpleAir enclosure and picks up heat from the laser counters and the ESP processor. PurpleAir's own guidance for outdoor units is to subtract roughly 8 °F (≈ 4.4 °C) from the reported value.
Particle-count entities — six size bins (≥0.3 / 0.5 / 1.0 / 2.5 / 5.0 / 10 µm), primary only, disabled by default (noisy but useful for power users). Enable individually from the device page.
Diagnostics (DIAGNOSTIC category) — WiFi signal, uptime, last-reported timestamp; free-heap and firmware-version are disabled by default. An online binary sensor mirrors the coordinator's last-poll status.
Config flow & options flow
- IP/hostname only — sensor's MAC (
SensorId) is the entry's unique ID, so a DHCP IP change can be fixed by editing the host in the options flow rather than re-adding the integration. The host edit re-probes the sensor and refuses to silently rebind to a different physical device. - All four user-tunable settings (scan interval, AQI corrections, two channel-disagreement thresholds) live in the options flow.
Diagnostics download — redacted dump (host, MAC, lat/lon, SSID stripped) including the last raw /json payload, useful for bug reports about firmware quirks.
HACS-ready — quality_scale silver, in-tree brand assets, 177 unit tests + a hassfest/HACS/pytest CI workflow.
Install via HACS
- HACS → Integrations → ⋮ → Custom repositories
- Add
https://github.com/jpettitt/purpleair-localas Integration - Install "PurpleAir Local" and restart Home Assistant
- Settings → Devices & Services → Add Integration → "PurpleAir Local"
- Enter the sensor's LAN IP. Repeat for each sensor.
Internal notes
- Compatible with firmware 7.02+ (tested against the user's two PA-II units on 7.02). Handles the doc-vs-firmware quirks documented in
DESIGN.md:pm2.5_aqi(literal dot, vs docs' underscore),place: inside/outside(vs docs'indoor/outdoor), and undocumented fields likepa_latencyandstatus_7. - Minimum Home Assistant: 2024.10 (where
config_entry=kwarg was added toDataUpdateCoordinator). - Single-laser sensors (some indoor PA-II units, e.g. the test unit reporting
2.0+BME280+PMSX003-A) skip channel-B entities and the disagreement binary sensor automatically; their "primary" entities are simply channel A.
Known limitations
- The EPA correction can read several AQI points high at very low humidity (<20 %) — a known property of the Barkjohn 2021 formula, not an integration bug.
- "Raw" AQI here uses the post-2024 EPA breakpoints, which differ from what the PurpleAir map and the on-device
pm2.5_aqifield show (those still use pre-2024 breakpoints). Same input PM, different AQI value — by design, for internal consistency. The literal on-device value is preserved in diagnostics downloads. - The EPA 5-piece extension for very high concentrations (used by AirNow Fire & Smoke Map) is not yet implemented; the simple Barkjohn formula underestimates above ~250 µg/m³.
- HACS validator's brand check is suppressed (
ignore: brands) because the brands repo stopped acceptingcustom_integrations/PRs in Feb 2026 — custom integrations now ship in-tree undercustom_components/<domain>/brand/, which this integration does.
Full changelog: https://github.com/jpettitt/purpleair-local/commits/v0.1.0