| """NWS station observations — latest hourly METAR for the nearest NYC airport. |
| |
| api.weather.gov/stations/{id}/observations/latest. |
| |
| Five NYC-region ASOS stations cover the city; we pick the nearest. |
| Most useful field for flood context is hourly precipitation (the |
| `precipitationLastHour` quantity, mm). The latest observation is |
| typically <60 min old. |
| """ |
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from math import asin, cos, radians, sin, sqrt |
|
|
| import httpx |
|
|
| DOC_ID = "nws_obs" |
| CITATION = "NWS station observations API (api.weather.gov/stations)" |
|
|
| USER_AGENT = "Riprap-NYC/0.1 (civic-flood-tool; +https://huggingface.co/spaces/msradam/riprap-nyc)" |
|
|
| |
| |
| |
| STATIONS = [ |
| |
| ("KNYC", "Central Park, NY", 40.7794, -73.9692), |
| ("KLGA", "LaGuardia Airport, NY", 40.7794, -73.8800), |
| ("KJFK", "JFK Airport, NY", 40.6413, -73.7781), |
| ("KEWR", "Newark Liberty, NJ", 40.6925, -74.1687), |
| ("KFRG", "Republic Farmingdale, NY", 40.7288, -73.4134), |
| |
| ("KHPN", "White Plains, NY", 41.0670, -73.7076), |
| ("KSWF", "Newburgh-Stewart, NY", 41.5042, -74.1048), |
| ("KPOU", "Poughkeepsie, NY", 41.6262, -73.8842), |
| ("KALB", "Albany Intl, NY", 42.7475, -73.8025), |
| ] |
|
|
|
|
| @dataclass |
| class Obs: |
| station_id: str |
| station_name: str |
| distance_km: float |
| obs_time: str | None |
| temp_c: float | None |
| precip_last_hour_mm: float | None |
| precip_last_3h_mm: float | None |
| precip_last_6h_mm: float | None |
| error: str | None = None |
|
|
|
|
| def _haversine_km(lat1, lon1, lat2, lon2) -> float: |
| R = 6371.0 |
| p1, p2 = radians(lat1), radians(lat2) |
| dp = radians(lat2 - lat1); dl = radians(lon2 - lon1) |
| a = sin(dp/2)**2 + cos(p1)*cos(p2)*sin(dl/2)**2 |
| return 2 * R * asin(sqrt(a)) |
|
|
|
|
| def _val_mm(props, key) -> float | None: |
| """NWS returns {value: ..., unitCode: 'wmoUnit:mm'} per quantity. Convert |
| to mm; if value is null, return None.""" |
| q = (props or {}).get(key) or {} |
| v = q.get("value") |
| if v is None: |
| return None |
| return round(float(v), 2) |
|
|
|
|
| def obs_at(lat: float, lon: float) -> Obs: |
| sid, name, slat, slon = min(STATIONS, key=lambda s: _haversine_km(lat, lon, s[2], s[3])) |
| dist_km = round(_haversine_km(lat, lon, slat, slon), 1) |
| out = Obs(station_id=sid, station_name=name, distance_km=dist_km, |
| obs_time=None, temp_c=None, |
| precip_last_hour_mm=None, precip_last_3h_mm=None, |
| precip_last_6h_mm=None) |
| try: |
| r = httpx.get( |
| f"https://api.weather.gov/stations/{sid}/observations/latest", |
| headers={"User-Agent": USER_AGENT, "Accept": "application/geo+json"}, |
| timeout=8.0, |
| ) |
| r.raise_for_status() |
| p = r.json().get("properties", {}) or {} |
| out.obs_time = p.get("timestamp") |
| out.temp_c = _val_mm(p, "temperature") |
| out.precip_last_hour_mm = _val_mm(p, "precipitationLastHour") |
| out.precip_last_3h_mm = _val_mm(p, "precipitationLast3Hours") |
| out.precip_last_6h_mm = _val_mm(p, "precipitationLast6Hours") |
| except Exception as e: |
| out.error = str(e) |
| return out |
|
|
|
|
| def summary_for_point(lat: float, lon: float) -> dict: |
| o = obs_at(lat, lon) |
| return { |
| "station_id": o.station_id, |
| "station_name": o.station_name, |
| "distance_km": o.distance_km, |
| "obs_time": o.obs_time, |
| "temp_c": o.temp_c, |
| "precip_last_hour_mm": o.precip_last_hour_mm, |
| "precip_last_3h_mm": o.precip_last_3h_mm, |
| "precip_last_6h_mm": o.precip_last_6h_mm, |
| "error": o.error, |
| } |
|
|