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