"""OpenMeteo API client — live weather data, no API key required.""" import httpx BASE_URL = "https://api.open-meteo.com/v1/forecast" # United hub coordinates HUB_COORDS: dict[str, tuple[float, float]] = { "ORD": (41.9742, -87.9073), # Chicago O'Hare "EWR": (40.6895, -74.1745), # Newark "IAH": (29.9902, -95.3368), # Houston Intercontinental "SFO": (37.6213, -122.3790), # San Francisco "DEN": (39.8561, -104.6737), # Denver "LAX": (33.9425, -118.4081), # Los Angeles "IAD": (38.9531, -77.4565), # Washington Dulles } # WMO weather interpretation codes WMO_CODES: dict[int, str] = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle", 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow", 77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers", 95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail", } SIGNIFICANT_CODES = {45, 48, 65, 75, 82, 86, 95, 96, 99} def _interpolate_waypoints( origin: tuple[float, float], dest: tuple[float, float], n: int = 2 ) -> list[tuple[float, float]]: """Generate n intermediate waypoints along a great-circle approximation.""" points = [] for i in range(1, n + 1): frac = i / (n + 1) lat = origin[0] + frac * (dest[0] - origin[0]) lon = origin[1] + frac * (dest[1] - origin[1]) points.append((round(lat, 4), round(lon, 4))) return points def _interpret_weather(code: int) -> dict: return { "code": code, "condition": WMO_CODES.get(code, f"Unknown ({code})"), "is_significant": code in SIGNIFICANT_CODES, } async def fetch_weather_at_point( lat: float, lon: float, client: httpx.AsyncClient ) -> dict: """Fetch current weather at a single point.""" params = { "latitude": lat, "longitude": lon, "current": "temperature_2m,wind_speed_10m,wind_direction_10m,weather_code,visibility,precipitation", "timezone": "UTC", } resp = await client.get(BASE_URL, params=params) resp.raise_for_status() data = resp.json() current = data["current"] weather = _interpret_weather(current["weather_code"]) return { "latitude": lat, "longitude": lon, "temperature_c": current["temperature_2m"], "wind_speed_kmh": current["wind_speed_10m"], "wind_direction_deg": current["wind_direction_10m"], "visibility_m": current.get("visibility"), "precipitation_mm": current.get("precipitation", 0), "weather": weather, } async def get_weather_along_route(origin: str, destination: str) -> dict: """Fetch weather at origin, destination, and en-route waypoints.""" origin_coords = HUB_COORDS.get(origin.upper()) dest_coords = HUB_COORDS.get(destination.upper()) if not origin_coords or not dest_coords: return { "error": f"Unknown airport code. Known: {', '.join(HUB_COORDS.keys())}", "origin": origin, "destination": destination, } waypoints = _interpolate_waypoints(origin_coords, dest_coords) all_points = [ ("origin", origin.upper(), origin_coords), *[(f"waypoint_{i+1}", f"WP{i+1}", wp) for i, wp in enumerate(waypoints)], ("destination", destination.upper(), dest_coords), ] results = {} significant_events = [] async with httpx.AsyncClient(timeout=10.0) as client: for label, name, (lat, lon) in all_points: try: weather = await fetch_weather_at_point(lat, lon, client) weather["label"] = label weather["name"] = name results[label] = weather if weather["weather"]["is_significant"]: significant_events.append( { "location": name, "condition": weather["weather"]["condition"], "code": weather["weather"]["code"], } ) except Exception as e: results[label] = {"label": label, "name": name, "error": str(e)} return { "origin": origin.upper(), "destination": destination.upper(), "waypoints": results, "significant_events": significant_events, "source": "openmeteo_live", } async def get_weather_forecast_hubs() -> dict: """Fetch 4-hour forecast for all United hubs.""" hubs = ["ORD", "EWR", "IAH", "SFO", "DEN"] results = {} async with httpx.AsyncClient(timeout=10.0) as client: for hub in hubs: lat, lon = HUB_COORDS[hub] params = { "latitude": lat, "longitude": lon, "hourly": "temperature_2m,wind_speed_10m,weather_code,visibility,precipitation_probability", "forecast_hours": 4, "timezone": "UTC", } try: resp = await client.get(BASE_URL, params=params) resp.raise_for_status() data = resp.json() hourly = data["hourly"] hours = [] risk_flag = False for i in range(len(hourly["time"])): code = hourly["weather_code"][i] weather = _interpret_weather(code) if weather["is_significant"]: risk_flag = True hours.append( { "time": hourly["time"][i], "temperature_c": hourly["temperature_2m"][i], "wind_speed_kmh": hourly["wind_speed_10m"][i], "weather": weather, "visibility_m": hourly.get("visibility", [None])[i] if "visibility" in hourly else None, "precipitation_probability": hourly[ "precipitation_probability" ][i], } ) results[hub] = { "hub": hub, "forecast": hours, "risk_flag": risk_flag, } except Exception as e: results[hub] = {"hub": hub, "error": str(e)} return {"hubs": results, "source": "openmeteo_live"}