Skip to content

v0.1.0 — initial release

Choose a tag to compare

@jpettitt jpettitt released this 18 May 05:44

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 /json endpoint 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_aqi field 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_disagreement binary 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

  1. HACS → Integrations → ⋮ → Custom repositories
  2. Add https://github.com/jpettitt/purpleair-local as Integration
  3. Install "PurpleAir Local" and restart Home Assistant
  4. Settings → Devices & Services → Add Integration → "PurpleAir Local"
  5. 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 like pa_latency and status_7.
  • Minimum Home Assistant: 2024.10 (where config_entry= kwarg was added to DataUpdateCoordinator).
  • 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_aqi field 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 accepting custom_integrations/ PRs in Feb 2026 — custom integrations now ship in-tree under custom_components/<domain>/brand/, which this integration does.

Full changelog: https://github.com/jpettitt/purpleair-local/commits/v0.1.0