File size: 3,886 Bytes
6a82282 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | """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)"
# NYC + Hudson Corridor ASOS stations. Picker is haversine-nearest, so adding
# upstate stations enables Albany / Poughkeepsie / Newburgh queries without
# breaking NYC behaviour (NYC stations stay closer for NYC lat/lon).
STATIONS = [
# NYC region
("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),
# Hudson Corridor (south → north)
("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,
}
|