Skip to content

NowCast EPA AQI for PM2.5 — Stage 2 (#2040)#2075

Merged
garthvh merged 1 commit into
meshtastic:mainfrom
bruschill:feature/2040-air-quality-nowcast
Jul 15, 2026
Merged

NowCast EPA AQI for PM2.5 — Stage 2 (#2040)#2075
garthvh merged 1 commit into
meshtastic:mainfrom
bruschill:feature/2040-air-quality-nowcast

Conversation

@bruschill

@bruschill bruschill commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Stage 2 of #2040 (cross-platform alignment: meshtastic/design#54). Computes a real EPA Air Quality Index from the PM2.5 history ingested in Stage 1 and wires it into the existing (previously unused) AirQualityIndex display.

Important

Stacked on #2074 (Stage 1) — merge that first. Until #2074 lands, this branch contains the Stage 1 commit too; once #2074 is merged this PR reduces to the single Stage 2 commit. Happy to rebase/retarget on request.

What's included

EPA math — Meshtastic/Helpers/EPAAirQuality.swift (new, pure + unit-tested)

  • NowCast: 12-hour weighted rolling PM2.5 average — weight factor = max(cMin/cMax, 0.5), NowCast = Σ(wⁱ·cᵢ) / Σ(wⁱ) with the most recent hour weighted heaviest. Returns nil unless at least 2 of the 3 most recent hours have data (EPA data-sufficiency rule).
  • aqi(fromPM25:): current (2024) EPA PM2.5 24-hour breakpoint table + Equation 1 (truncate concentration to 1 decimal → linear interpolation → round), clamped to the app's 0–500 Aqi scale. Verified against the EPA AQS breakpoint code table (parameter 88101), including the collapsed 225.5–325.4 → 301–500 hazardous row.
  • nowCastAQI(from:): buckets timestamped readings into the most recent 12 clock hours (averaging within each hour), runs NowCast, maps to AQI.

Wiring

  • NodeInfoEntity.airQualityNowCastAQI — pulls the last 12h of PM2.5 telemetry and returns the NowCast AQI, or nil when history is insufficient.
  • Node detail Air Quality section — headlines the NowCast AQI gauge (AirQualityIndex) above the raw PM readings when available; when there isn't enough history the gauge is omitted and only the Stage 1 raw readings show (design#54: never a misleading instantaneous AQI).
  • AirQualityIndex — fixes a pre-existing bug (used IaqDisplayMode instead of AqiDisplayMode) and corrects the hardcoded "IAQ" labels to "AQI". This view now has its first real caller.

Tests

EPAAirQualityTests (7 tests): breakpoint boundaries, truncation/clamping, NowCast weighting + the 2-of-3 data-sufficiency gate, all-zero handling, 12h-window exclusion, and end-to-end nowCastAQI from timestamped readings.

Verification

  • xcodebuild BUILD SUCCEEDED; all 11 air-quality tests pass; SwiftLint clean on changed files (one pre-existing type_body_length warning on the large NodeDetail view remains); sourcekit-lsp diagnostics clean.

Design note

Uses the current post-2024 EPA breakpoints (per the official EPA/AirNow standard design#54 defers to). Since neither iOS nor Android had shipped a computed index, there's no prior platform table to match — following the current official EPA table is the convergence point for cross-platform consistency.

Closes #2040 (Stage 2, with Stage 1 in #2074).

Summary by CodeRabbit

  • New Features
    • Added EPA-based Air Quality Index (AQI) calculations using recent PM2.5 readings.
    • Added an AQI gauge to node details when sufficient air-quality history is available.
    • Added localized AQI labels and values.
  • Bug Fixes
    • Updated air-quality terminology from “IAQ” to “AQI” throughout the display.
  • Tests
    • Added coverage for AQI thresholds, NowCast calculations, invalid data, and insufficient history.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 951ec4a5-3db3-452f-b17b-a07092603ed3

📥 Commits

Reviewing files that changed from the base of the PR and between 484a2fd and d6d3aa4.

📒 Files selected for processing (7)
  • Localizable.xcstrings
  • Meshtastic.xcodeproj/project.pbxproj
  • Meshtastic/Extensions/SwiftData/NodeInfoEntityExtension.swift
  • Meshtastic/Helpers/EPAAirQuality.swift
  • Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift
  • Meshtastic/Views/Nodes/Helpers/NodeDetail.swift
  • MeshtasticTests/MeshPacketsAndTelemetryTests.swift
👮 Files not reviewed due to content moderation or server errors (7)
  • Meshtastic/Helpers/EPAAirQuality.swift
  • MeshtasticTests/MeshPacketsAndTelemetryTests.swift
  • Meshtastic.xcodeproj/project.pbxproj
  • Meshtastic/Extensions/SwiftData/NodeInfoEntityExtension.swift
  • Meshtastic/Views/Nodes/Helpers/NodeDetail.swift
  • Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift
  • Localizable.xcstrings

📝 Walkthrough

Walkthrough

Adds EPA PM2.5 NowCast AQI computation, derives AQI from recent node telemetry, and conditionally displays the result in node details. AQI terminology and localized strings are updated, with tests covering breakpoint conversion, weighting, history requirements, and stale readings.

Changes

EPA NowCast AQI

Layer / File(s) Summary
EPA AQI and NowCast calculation
Meshtastic/Helpers/EPAAirQuality.swift, MeshtasticTests/MeshPacketsAndTelemetryTests.swift, Meshtastic.xcodeproj/project.pbxproj
Adds EPA PM2.5 breakpoints, AQI conversion, NowCast weighting and timestamp bucketing, with coverage for boundaries, truncation, clamping, insufficient history, zero values, and stale readings.
Node telemetry AQI integration
Meshtastic/Extensions/SwiftData/NodeInfoEntityExtension.swift
Derives a node’s NowCast AQI from PM2.5 telemetry within the previous 12 hours and returns no AQI when history is insufficient.
AQI node detail presentation
Meshtastic/Views/Nodes/Helpers/NodeDetail.swift, Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift, Localizable.xcstrings
Synchronizes the computed AQI into node detail state, conditionally shows the AQI gauge, and changes display labels and localization entries from IAQ to AQI.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NodeDetail
  participant NodeInfoEntity
  participant EPAAirQuality
  NodeDetail->>NodeInfoEntity: refresh node summary
  NodeInfoEntity->>EPAAirQuality: provide recent PM2.5 readings
  EPAAirQuality-->>NodeInfoEntity: return NowCast AQI or nil
  NodeInfoEntity-->>NodeDetail: update AQI state
  NodeDetail-->>NodeDetail: render AQI gauge when available
Loading

Possibly related PRs

Suggested reviewers: garthvh

Poem

I’m a rabbit with air in my ear,
NowCast numbers hop into view.
PM dots gather, twelve hours near,
EPA colors bloom bright and true.
AQI carrots for every node!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the Stage 2 NowCast EPA AQI work.
Description check ✅ Passed The description covers the change, rationale, testing, and verification, with only non-critical template sections omitted.
Linked Issues check ✅ Passed The changes implement the Stage 2 objectives: NowCast PM2.5 AQI math, AQI mapping, display wiring, and fallback behavior.
Out of Scope Changes check ✅ Passed The diff stays focused on AQI math, UI wiring, localization, build wiring, and tests, with no clear unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Stage 2 of meshtastic#2040 / design#54: compute a real Air Quality Index from the PM2.5
history ingested in Stage 1 and wire it into the existing (previously unused)
AirQualityIndex display.

- EPAAirQuality: new, pure/testable EPA math —
  * NowCast: 12h weighted rolling PM2.5 average (weight = max(cMin/cMax, 0.5)),
    requiring 2 of the 3 most recent hours of data or it returns nil.
  * aqi(fromPM25:): current (2024) EPA PM2.5 24-hour breakpoint table + Equation 1
    (truncate to 1 decimal, linear interpolation, round), clamped to the app's
    0–500 Aqi scale.
  * nowCastAQI(from:): buckets timestamped readings into the last 12 clock hours,
    runs NowCast, maps to AQI.
- NodeInfoEntity.airQualityNowCastAQI: pulls the last 12h of PM2.5 telemetry and
  returns the NowCast AQI, or nil when there isn't enough history.
- NodeDetail Air Quality section: shows the computed AQI via AirQualityIndex when
  available; otherwise falls back to the Stage 1 raw PM readings (design#54 — never
  show a misleading instantaneous AQI).
- AirQualityIndex: fix the long-standing bug where it used IaqDisplayMode instead
  of AqiDisplayMode, and correct the hardcoded "IAQ" labels to "AQI". This view now
  has its first real caller.
- Tests: EPAAirQualityTests — breakpoint boundaries, truncation/clamping, NowCast
  weighting + data-sufficiency, and end-to-end nowCastAQI from timestamped readings.
- Localization: register the new AQI label strings.
@garthvh
garthvh force-pushed the feature/2040-air-quality-nowcast branch from 53473b3 to d6d3aa4 Compare July 15, 2026 07:50
@garthvh
garthvh marked this pull request as ready for review July 15, 2026 15:37
@garthvh garthvh added the skip-docs-check Use this label to skip the automatic docs audit label Jul 15, 2026

@garthvh garthvh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified: rebased cleanly to a single Stage-2 commit on current main (Stage 1 landed in #2074), 7-file delta, full app build green, and the EPA/NowCast + AQI export/boundary suites pass (23 tests). Breakpoint table is the current 2024 EPA PM2.5 curve per the agreed direction; Android alignment to the same curve is tracked in meshtastic/Meshtastic-Android#6272 to land before release.

@garthvh
garthvh merged commit c504bc8 into meshtastic:main Jul 15, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-docs-check Use this label to skip the automatic docs audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ingest PM2.5/PM10 telemetry and compute NowCast AQI

2 participants