NowCast EPA AQI for PM2.5 — Stage 2 (#2040)#2075
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
👮 Files not reviewed due to content moderation or server errors (7)
📝 WalkthroughWalkthroughAdds 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. ChangesEPA NowCast 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
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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. Comment |
726601c to
53473b3
Compare
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.
53473b3 to
d6d3aa4
Compare
garthvh
left a comment
There was a problem hiding this comment.
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.
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)
AirQualityIndexdisplay.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)max(cMin/cMax, 0.5),NowCast = Σ(wⁱ·cᵢ) / Σ(wⁱ)with the most recent hour weighted heaviest. Returnsnilunless 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–500Aqiscale. Verified against the EPA AQS breakpoint code table (parameter 88101), including the collapsed225.5–325.4 → 301–500hazardous 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, ornilwhen history is insufficient.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 (usedIaqDisplayModeinstead ofAqiDisplayMode) 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-endnowCastAQIfrom timestamped readings.Verification
xcodebuildBUILD SUCCEEDED; all 11 air-quality tests pass; SwiftLint clean on changed files (one pre-existingtype_body_lengthwarning on the largeNodeDetailview 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