fix:airnow:Migrate AirNow app to 2026 observation API.#589
Conversation
The latLong/current endpoint retires September 30, 2026; normalize the new ziplatlong response format for existing display logic. Co-authored-by: Cursor <cursoragent@cursor.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe AirNow app now fetches observations from a different endpoint, normalizes AQI and parameter data, handles unknown categories during rendering, replaces both WebP assets, and updates the manifest timestamp. ChangesAirNow update
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant get_current_observation
participant AirNow_API
participant normalize_observation
participant main
get_current_observation->>AirNow_API: Request current observation
AirNow_API-->>get_current_observation: Return raw observation items
get_current_observation->>normalize_observation: Normalize each item
normalize_observation-->>get_current_observation: Return standardized PM2.5 observation
get_current_observation-->>main: Provide normalized observation
main->>main: Render unknown category when category number is -1
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/airnow/airnow.star`:
- Around line 29-37: Update the category lookup using CATEGORY_NAME_TO_NUMBER
and the downstream get_alert_colors flow so unknown aqiCategoryName values do
not become 0 and render as Hazardous. Represent unmapped categories explicitly
as -1, then handle -1 before calling get_alert_colors by skipping the
observation or using a distinct unknown presentation; preserve the existing
mapping and Hazardous behavior for recognized categories.
- Around line 53-84: The normalize_observation function must read AirNow’s
PascalCase fields: use DateObserved, HourObserved, LocalTimeZone, ReportingArea,
ParameterName, AQI, and CategoryName in its lookups. Preserve the existing hour
parsing, ozone normalization, category-number mapping, and output structure so
get_current_observation can match PM2.5 correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 36efc771-db65-46db-80b1-e4f14173cb3e
📒 Files selected for processing (4)
apps/airnow/airnow.starapps/airnow/airnow.webpapps/airnow/airnow@2x.webpapps/airnow/manifest.yaml
| return "https://www.airnowapi.org/aq/observation/current/ziplatlong/?format=application/json&latitude={lat}&longitude={lng}&api_key={api_key}".format( | ||
| lat = lat, | ||
| lng = lng, | ||
| api_key = api_key, | ||
| ) | ||
|
|
||
| def normalize_observation(raw): | ||
| hour_raw = raw.get("hourObserved", 0) | ||
| if type(hour_raw) == "string": | ||
| hour = int(hour_raw.split(":")[0]) | ||
| else: | ||
| hour = int(hour_raw) | ||
|
|
||
| category_name = raw.get("aqiCategoryName", "") | ||
| category_number = CATEGORY_NAME_TO_NUMBER.get(category_name, 0) | ||
|
|
||
| parameter_name = raw.get("parameterName", "") | ||
| if parameter_name == "OZONE": | ||
| parameter_name = "O3" | ||
|
|
||
| return { | ||
| "DateObserved": raw.get("dateObserved", ""), | ||
| "HourObserved": hour, | ||
| "LocalTimeZone": raw.get("localTimeZone", ""), | ||
| "ReportingArea": raw.get("reportingAreaName", ""), | ||
| "ParameterName": parameter_name, | ||
| "AQI": raw.get("nowcastAQI", raw.get("aqi", -1)), | ||
| "Category": { | ||
| "Number": category_number, | ||
| "Name": category_name, | ||
| }, | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== apps/airnow/airnow.star (excerpt) ==\n'
cat -n apps/airnow/airnow.star | sed -n '1,220p'
printf '\n== repo search for ziplatlong / AirNow field names ==\n'
rg -n --hidden -S 'ziplatlong|hourObserved|dateObserved|localTimeZone|reportingAreaName|aqiCategoryName|nowcastAQI|CategoryName|ReportingArea' .Repository: tronbyt/apps
Length of output: 9086
🌐 Web query:
AirNow API ziplatlong current observation JSON field names DateObserved HourObserved LocalTimeZone ReportingArea ParameterName AQI CategoryNumber CategoryName
💡 Result:
As of July 20, 2026, the AirNow API has recently undergone changes. In June 2026, AirNow retired certain legacy API endpoints for current observations and forecasts and replaced them with updated versions [1][2]. For legacy integrations, the response format for current observations typically included the following fields: DateObserved, HourObserved, LocalTimeZone, ReportingArea, ParameterName, AQI, and a nested Category object containing Number and Name [3][4]. However, the new 2026 API endpoints (e.g., /aq/observation/current/ziplatlong) have introduced changes to the output format [2]. Key changes noted in updated technical implementations include: - Field name changes from PascalCase (e.g., dateObserved) to camelCase (e.g., dateObserved) [1][2]. - Replacement of the AQI field with nowcastAQI [1][2]. - Changes to the Category structure and normalization of parameter names (e.g., OZONE to O3) [2]. - Conversion of HourObserved from a string format (e.g., 15:00) to an integer (e.g., 15) [2]. Developers should refer to the official AirNow API documentation for the most current schemas, as the transition to these new endpoints is mandatory following the retirement of the legacy services on September 30, 2026 [2].
Citations:
- 1: https://github.com/asymworks/pyairnow
- 2: Migrate to AirNow 2026 API endpoints asymworks/pyairnow#18
- 3: https://github.com/briandconnelly/airnow-py
- 4: AirNow integration missing attributes home-assistant/core#101921
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n apps/airnow/airnow.star | sed -n '1,180p'Repository: tronbyt/apps
Length of output: 6822
🌐 Web query:
site:airnowapi.org ziplatlong current observation JSON DateObserved HourObserved LocalTimeZone ReportingArea ParameterName AQI CategoryNumber CategoryName
💡 Result:
The AirNow API provides current air quality observations by zip code or latitude/longitude through its web services [1]. The JSON output for these observations typically includes the following fields: DateObserved: The date the air quality measurement was observed [2][3]. HourObserved: The hour the measurement was observed [2][3]. LocalTimeZone: The local time zone associated with the reporting area [2]. ReportingArea: The name of the city or region for which the air quality data is reported [2]. ParameterName: The specific pollutant measured, such as OZONE, PM2.5, or PM10 [2][3]. AQI: The calculated Air Quality Index value [2]. CategoryNumber: An integer (1-6) representing the AQI health category [4]. CategoryName: The descriptive name corresponding to the AQI category (e.g., Good, Moderate, Unhealthy) [4]. When requesting current observations by zip code or latitude/longitude, the API identifies the relevant Reporting Area assigned by local air quality agencies and returns the peak AQI value for that area [5][6]. Note that these observations are preliminary and are intended for public reporting of the AQI, not for regulatory use [7]. The AQI categories are standardized, ranging from 1 (Good) to 6 (Hazardous) [4].
Citations:
- 1: https://docs.airnowapi.org/webservices
- 2: https://docs.airnowapi.org/docs/HourlyAQObsFactSheet.pdf
- 3: https://docs.airnowapi.org/docs/HourlyDataFactSheet.pdf
- 4: https://docs.airnowapi.org/aq101
- 5: https://docs.airnowapi.org/faq
- 6: http://airnowapi.org/faq
- 7: https://docs.airnowapi.org/
🌐 Web query:
site:developer.airnowapi.org ziplatlong observation current response fields
💡 Result:
AirNow’s official docs only say that Current Observations by Zip Code / LatLong return current AQI values and categories for a reporting area; the overview page doesn’t publish the full field schema there. The fact sheet says outputs can be returned in CSV, JSON, or XML, and that each service’s documentation page defines its input/output parameters. (docs.airnowapi.org)
The FAQ also notes these web services return the peak AQI for a reporting area and are meant for end-user lookup, not bulk database building. (docs.airnowapi.org)
If you want, I can help find the exact endpoint docs/sample response for the zip/lat-long observation call.
Citations:
Use AirNow’s PascalCase response keys here. normalize_observation is reading lowercase names (dateObserved, hourObserved, localTimeZone, reportingAreaName, parameterName, nowcastAQI, aqiCategoryName), but current observation payloads use DateObserved, HourObserved, LocalTimeZone, ReportingArea, ParameterName, AQI, and CategoryName. These lookups will miss and the app will fall back to the default values, so get_current_observation won’t match PM2.5.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/airnow/airnow.star` around lines 53 - 84, The normalize_observation
function must read AirNow’s PascalCase fields: use DateObserved, HourObserved,
LocalTimeZone, ReportingArea, ParameterName, AQI, and CategoryName in its
lookups. Preserve the existing hour parsing, ozone normalization,
category-number mapping, and output structure so get_current_observation can
match PM2.5 correctly.
Unmapped aqiCategoryName values now use -1 and show an explicit error instead of falling through to the Hazardous color path. Co-authored-by: Cursor <cursoragent@cursor.com>
The latLong/current endpoint retires September 30, 2026; normalize the new ziplatlong response format for existing display logic.
Summary by CodeRabbit