Spaces:
Running
Running
| import httpx | |
| import asyncio | |
| import os | |
| from datetime import datetime | |
| from typing import Optional, Dict, Any, List | |
| from async_lru import alru_cache | |
| from services.api_client import get_client | |
| from models.weather_models import CurrentWeatherModel, AstroModel, AirQualityModel | |
| from utils.conversions import ( | |
| convert_temperature, | |
| convert_wind_speed, | |
| convert_precip, | |
| convert_visibility, | |
| ) | |
| OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast" | |
| # Weatherstack weather codes mapping (simplified) | |
| # 113: Clear, 116: Partly Cloudy, 119: Cloudy, 122: Overcast, etc. | |
| WMO_TO_WEATHERSTACK = { | |
| 0: (113, "Clear/Sunny"), | |
| 1: (116, "Partly Cloudy"), | |
| 2: (116, "Partly Cloudy"), | |
| 3: (119, "Cloudy"), | |
| 45: (248, "Fog"), | |
| 48: (260, "Freezing Fog"), | |
| 51: (263, "Patchy Light Drizzle"), | |
| 53: (266, "Light Drizzle"), | |
| 55: (284, "Heavy Freezing Drizzle"), | |
| 61: (296, "Light Rain"), | |
| 63: (302, "Moderate Rain"), | |
| 65: (308, "Heavy Rain"), | |
| 71: (326, "Light Snow"), | |
| 73: (332, "Moderate Snow"), | |
| 75: (338, "Heavy Snow"), | |
| 95: (389, "Moderate/Heavy Rain with Thunder"), | |
| } | |
| AIR_QUALITY_URL = "https://air-quality-api.open-meteo.com/v1/air-quality" | |
| WEATHERSTACK_API_URL = "http://api.weatherstack.com" | |
| # Cache for 24 hours since astro data doesn't change frequently | |
| async def fetch_astro_from_weatherstack( | |
| lat: float, lon: float, date: str | |
| ) -> Optional[AstroModel]: | |
| """Fetch astronomical data from Weatherstack API.""" | |
| # Load API key from environment (checks both os.environ and .env file) | |
| api_key = os.getenv("WEATHERSTACK_ACCESS_KEY") | |
| if not api_key: | |
| # Fallback to loading from .env file directly | |
| try: | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| api_key = os.getenv("WEATHERSTACK_ACCESS_KEY") | |
| except: | |
| pass | |
| if not api_key: | |
| return None | |
| params = { | |
| "access_key": api_key, | |
| "query": f"{lat},{lon}", | |
| "historical_date": date, | |
| } | |
| try: | |
| client = get_client() | |
| response = await client.get(f"{WEATHERSTACK_API_URL}/historical", params=params) | |
| if response.status_code == 200: | |
| data = response.json() | |
| historical = data.get("historical", {}) | |
| day_data = historical.get(date, {}) | |
| astro_data = day_data.get("astro", {}) | |
| if astro_data: | |
| return AstroModel( | |
| sunrise=astro_data.get("sunrise", "Unknown"), | |
| sunset=astro_data.get("sunset", "Unknown"), | |
| moonrise=astro_data.get("moonrise", "Unknown"), | |
| moonset=astro_data.get("moonset", "Unknown"), | |
| moon_phase=astro_data.get("moon_phase", "Unknown"), | |
| moon_illumination=int(astro_data.get("moon_illumination", 0)), | |
| ) | |
| else: | |
| print(f"Debug - Weatherstack response missing astro data. Response: {data}") | |
| else: | |
| print(f"Debug - Weatherstack API returned status {response.status_code}: {response.text}") | |
| except Exception as e: | |
| print(f"Error fetching Weatherstack astro data: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return None | |
| async def fetch_current_weather( | |
| lat: float, lon: float, unit: str = "m" | |
| ) -> CurrentWeatherModel: | |
| params = { | |
| "latitude": lat, | |
| "longitude": lon, | |
| "current": "temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,weather_code,cloud_cover,pressure_msl,wind_speed_10m,wind_direction_10m,uv_index,visibility", | |
| "daily": "sunrise,sunset", | |
| "timezone": "auto", | |
| } | |
| aq_params = { | |
| "latitude": lat, | |
| "longitude": lon, | |
| "current": "carbon_monoxide,nitrogen_dioxide,ozone,sulphur_dioxide,pm2_5,pm10,us_aqi,european_aqi", | |
| "timezone": "auto", | |
| } | |
| client = get_client() | |
| weather_res, aq_res = await asyncio.gather( | |
| client.get(OPEN_METEO_URL, params=params), | |
| client.get(AIR_QUALITY_URL, params=aq_params), | |
| ) | |
| if weather_res.status_code == 200: | |
| data = weather_res.json() | |
| current = data["current"] | |
| daily = data.get("daily", {}) | |
| aq_data = None | |
| if aq_res.status_code == 200: | |
| aq_json = aq_res.json()["current"] | |
| aq_data = AirQualityModel( | |
| co=str(round(aq_json["carbon_monoxide"], 1)), | |
| no2=str(round(aq_json["nitrogen_dioxide"], 1)), | |
| o3=str(round(aq_json["ozone"], 1)), | |
| so2=str(round(aq_json["sulphur_dioxide"], 1)), | |
| pm2_5=str(round(aq_json["pm2_5"], 1)), | |
| pm10=str(round(aq_json["pm10"], 1)), | |
| us_epa_index=str(aq_json["us_aqi"]), | |
| gb_defra_index=str( | |
| aq_json["european_aqi"] | |
| ), # Using EU AQI as proxy for DEFRA | |
| ) | |
| wstack_code, wstack_desc = WMO_TO_WEATHERSTACK.get( | |
| current["weather_code"], (113, "Clear") | |
| ) | |
| # Astro data from Weatherstack | |
| astro = None | |
| if daily: | |
| current_date = datetime.fromisoformat(current["time"]).strftime("%Y-%m-%d") | |
| astro = await fetch_astro_from_weatherstack(lat, lon, current_date) | |
| # Fallback to Open-Meteo sunrise/sunset if Weatherstack fails | |
| if not astro: | |
| astro = AstroModel( | |
| sunrise=datetime.fromisoformat(daily["sunrise"][0]).strftime( | |
| "%I:%M %p" | |
| ), | |
| sunset=datetime.fromisoformat(daily["sunset"][0]).strftime( | |
| "%I:%M %p" | |
| ), | |
| moonrise="Unknown", | |
| moonset="Unknown", | |
| moon_phase="Unknown", | |
| moon_illumination=0, | |
| ) | |
| return CurrentWeatherModel( | |
| observation_time=datetime.fromisoformat(current["time"]).strftime( | |
| "%I:%M %p" | |
| ), | |
| temperature=convert_temperature(current["temperature_2m"], unit), | |
| weather_code=wstack_code, | |
| weather_icons=[ | |
| f"https://api.yourweatherservice.com/images/wsymbol_{wstack_code:04d}.png" | |
| ], | |
| weather_descriptions=[wstack_desc], | |
| wind_speed=convert_wind_speed(current["wind_speed_10m"], unit), | |
| wind_degree=current["wind_direction_10m"], | |
| wind_dir=get_wind_dir(current["wind_direction_10m"]), | |
| pressure=int(current["pressure_msl"]), | |
| precip=convert_precip(current["precipitation"], unit), | |
| humidity=current["relative_humidity_2m"], | |
| cloudcover=current["cloud_cover"], | |
| feelslike=convert_temperature(current["apparent_temperature"], unit), | |
| uv_index=int(current["uv_index"]), | |
| visibility=convert_visibility( | |
| current["visibility"] / 1000, unit | |
| ), # meters to km | |
| is_day="yes" if current["is_day"] else "no", | |
| astro=astro, | |
| air_quality=aq_data, | |
| ) | |
| raise Exception("Failed to fetch weather data") | |
| def get_wind_dir(degrees: float) -> str: | |
| directions = [ | |
| "N", | |
| "NNE", | |
| "NE", | |
| "ENE", | |
| "E", | |
| "ESE", | |
| "SE", | |
| "SSE", | |
| "S", | |
| "SSW", | |
| "SW", | |
| "WSW", | |
| "W", | |
| "WNW", | |
| "NW", | |
| "NNW", | |
| ] | |
| idx = int((degrees + 11.25) / 22.5) % 16 | |
| return directions[idx] | |
| async def fetch_weather_forecast( | |
| lat: float, lon: float, days: int, unit: str = "m" | |
| ) -> Dict[str, Any]: | |
| from models.weather_models import HistoricalDayModel, AstroModel | |
| params = { | |
| "latitude": lat, | |
| "longitude": lon, | |
| "daily": "temperature_2m_max,temperature_2m_min,sunrise,sunset,uv_index_max,precipitation_sum,snowfall_sum,weather_code", | |
| "forecast_days": days, | |
| "timezone": "auto", | |
| } | |
| client = get_client() | |
| response = await client.get(OPEN_METEO_URL, params=params) | |
| if response.status_code == 200: | |
| data = response.json() | |
| daily = data["daily"] | |
| forecast_data = {} | |
| for i in range(len(daily["time"])): | |
| date_str = daily["time"][i] | |
| wstack_code, _ = WMO_TO_WEATHERSTACK.get( | |
| daily["weather_code"][i], (113, "Clear") | |
| ) | |
| # Fetch complete astro data from Weatherstack | |
| astro = await fetch_astro_from_weatherstack(lat, lon, date_str) | |
| # Fallback to Open-Meteo if Weatherstack fails | |
| if not astro: | |
| astro = AstroModel( | |
| sunrise=datetime.fromisoformat(daily["sunrise"][i]).strftime( | |
| "%I:%M %p" | |
| ), | |
| sunset=datetime.fromisoformat(daily["sunset"][i]).strftime( | |
| "%I:%M %p" | |
| ), | |
| moonrise="Unknown", | |
| moonset="Unknown", | |
| moon_phase="Unknown", | |
| moon_illumination=0, | |
| ) | |
| forecast_data[date_str] = HistoricalDayModel( | |
| date=date_str, | |
| date_epoch=int(datetime.fromisoformat(date_str).timestamp()), | |
| astro=astro, | |
| mintemp=convert_temperature(daily["temperature_2m_min"][i], unit), | |
| maxtemp=convert_temperature(daily["temperature_2m_max"][i], unit), | |
| avgtemp=convert_temperature( | |
| ( | |
| daily["temperature_2m_min"][i] | |
| + daily["temperature_2m_max"][i] | |
| ) | |
| / 2, | |
| unit, | |
| ), | |
| totalsnow=convert_precip(daily["snowfall_sum"][i], unit), | |
| sunhour=10.0, | |
| uv_index=int( | |
| daily.get("uv_index_max", [3] * len(daily["time"]))[i] | |
| ), | |
| ) | |
| return forecast_data | |
| return {} | |
| HISTORICAL_API_URL = "https://archive-api.open-meteo.com/v1/archive" | |
| async def fetch_historical_weather( | |
| lat: float, lon: float, date: str, unit: str = "m" | |
| ) -> Dict[str, Any]: | |
| from models.weather_models import HistoricalDayModel, AstroModel | |
| params = { | |
| "latitude": lat, | |
| "longitude": lon, | |
| "start_date": date, | |
| "end_date": date, | |
| "daily": "temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_sum,snowfall_sum,weather_code", | |
| "timezone": "auto", | |
| } | |
| client = get_client() | |
| response = await client.get(HISTORICAL_API_URL, params=params) | |
| if response.status_code == 200: | |
| data = response.json() | |
| daily = data["daily"] | |
| historical_data = {} | |
| if daily["time"]: | |
| date_str = daily["time"][0] | |
| wstack_code, _ = WMO_TO_WEATHERSTACK.get( | |
| daily["weather_code"][0], (113, "Clear") | |
| ) | |
| # Fetch complete astro data from Weatherstack | |
| astro = await fetch_astro_from_weatherstack(lat, lon, date_str) | |
| # Fallback to Open-Meteo if Weatherstack fails | |
| if not astro: | |
| astro = AstroModel( | |
| sunrise=datetime.fromisoformat(daily["sunrise"][0]).strftime( | |
| "%I:%M %p" | |
| ), | |
| sunset=datetime.fromisoformat(daily["sunset"][0]).strftime( | |
| "%I:%M %p" | |
| ), | |
| moonrise="Unknown", | |
| moonset="Unknown", | |
| moon_phase="Unknown", | |
| moon_illumination=0, | |
| ) | |
| historical_data[date_str] = HistoricalDayModel( | |
| date=date_str, | |
| date_epoch=int(datetime.fromisoformat(date_str).timestamp()), | |
| astro=astro, | |
| mintemp=convert_temperature(daily["temperature_2m_min"][0], unit), | |
| maxtemp=convert_temperature(daily["temperature_2m_max"][0], unit), | |
| avgtemp=convert_temperature( | |
| ( | |
| daily["temperature_2m_min"][0] | |
| + daily["temperature_2m_max"][0] | |
| ) | |
| / 2, | |
| unit, | |
| ), | |
| totalsnow=convert_precip(daily["snowfall_sum"][0], unit), | |
| sunhour=10.0, | |
| uv_index=3, | |
| ) | |
| return historical_data | |
| return {} | |