|
|
|
@@ -43,13 +43,18 @@ class EnvironmentService:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Handle wind direction - convert degrees to cardinal if integer
|
|
|
|
|
wind_dir = raw_data.get("wind_direction") or raw_data.get("wind_dir")
|
|
|
|
|
if isinstance(wind_dir, (int, float)):
|
|
|
|
|
wind_dir = self._degrees_to_cardinal(wind_dir)
|
|
|
|
|
|
|
|
|
|
return WeatherData(
|
|
|
|
|
temperature=raw_data.get("temperature") or raw_data.get("temp"),
|
|
|
|
|
feels_like=raw_data.get("feels_like") or raw_data.get("feelslike"),
|
|
|
|
|
conditions=raw_data.get("conditions") or raw_data.get("weather") or raw_data.get("description"),
|
|
|
|
|
humidity=raw_data.get("humidity"),
|
|
|
|
|
wind_speed=raw_data.get("wind_speed") or raw_data.get("windspeed") or raw_data.get("wind"),
|
|
|
|
|
wind_direction=raw_data.get("wind_direction") or raw_data.get("wind_dir"),
|
|
|
|
|
wind_direction=wind_dir,
|
|
|
|
|
pressure=raw_data.get("pressure"),
|
|
|
|
|
visibility=raw_data.get("visibility"),
|
|
|
|
|
uv_index=raw_data.get("uv_index") or raw_data.get("uv"),
|
|
|
|
@@ -60,6 +65,13 @@ class EnvironmentService:
|
|
|
|
|
logger.warning(f"Failed to parse weather data: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _degrees_to_cardinal(self, degrees: float) -> str:
|
|
|
|
|
"""Convert wind direction degrees to cardinal direction."""
|
|
|
|
|
directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
|
|
|
|
|
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
|
|
|
|
|
index = round(degrees / 22.5) % 16
|
|
|
|
|
return directions[index]
|
|
|
|
|
|
|
|
|
|
def _parse_forecast(self, raw_data: Any) -> Optional[List[ForecastDay]]:
|
|
|
|
|
"""
|
|
|
|
|
Parse raw forecast data into list of ForecastDay schemas.
|
|
|
|
@@ -73,7 +85,15 @@ class EnvironmentService:
|
|
|
|
|
# Normalize to list
|
|
|
|
|
forecast_list = raw_data
|
|
|
|
|
if isinstance(raw_data, dict):
|
|
|
|
|
forecast_list = raw_data.get("days") or raw_data.get("forecast") or []
|
|
|
|
|
# Check 'daily' first (scheduler format), then 'forecast', then 'days'
|
|
|
|
|
# Note: 'days' might be an integer count, so check 'daily' first
|
|
|
|
|
forecast_list = raw_data.get("daily") or raw_data.get("forecast")
|
|
|
|
|
if forecast_list is None:
|
|
|
|
|
days_value = raw_data.get("days")
|
|
|
|
|
if isinstance(days_value, list):
|
|
|
|
|
forecast_list = days_value
|
|
|
|
|
else:
|
|
|
|
|
forecast_list = []
|
|
|
|
|
|
|
|
|
|
if not isinstance(forecast_list, list):
|
|
|
|
|
return None
|
|
|
|
@@ -83,8 +103,8 @@ class EnvironmentService:
|
|
|
|
|
if isinstance(day, dict):
|
|
|
|
|
days.append(ForecastDay(
|
|
|
|
|
date=day.get("date", ""),
|
|
|
|
|
high=day.get("high") or day.get("maxtemp") or day.get("temp_max"),
|
|
|
|
|
low=day.get("low") or day.get("mintemp") or day.get("temp_min"),
|
|
|
|
|
high=day.get("high") or day.get("temp_high") or day.get("maxtemp") or day.get("temp_max"),
|
|
|
|
|
low=day.get("low") or day.get("temp_low") or day.get("mintemp") or day.get("temp_min"),
|
|
|
|
|
conditions=day.get("conditions") or day.get("weather") or day.get("description"),
|
|
|
|
|
precipitation_chance=day.get("precipitation_chance") or day.get("pop") or day.get("precip"),
|
|
|
|
|
icon=day.get("icon"),
|
|
|
|
@@ -106,19 +126,39 @@ class EnvironmentService:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
sunrise = raw_data.get("sunrise")
|
|
|
|
|
sunset = raw_data.get("sunset")
|
|
|
|
|
# Prefer ISO format fields (sunrise_iso, sunset_iso) over time-only fields
|
|
|
|
|
sunrise = raw_data.get("sunrise_iso") or raw_data.get("sunrise")
|
|
|
|
|
sunset = raw_data.get("sunset_iso") or raw_data.get("sunset")
|
|
|
|
|
|
|
|
|
|
# Parse datetime strings if needed
|
|
|
|
|
if isinstance(sunrise, str):
|
|
|
|
|
sunrise = datetime.fromisoformat(sunrise.replace("Z", "+00:00"))
|
|
|
|
|
# Handle time-only format (HH:MM) by combining with today's date
|
|
|
|
|
if len(sunrise) <= 5 and ":" in sunrise:
|
|
|
|
|
today = datetime.now().date()
|
|
|
|
|
sunrise = datetime.strptime(f"{today} {sunrise}", "%Y-%m-%d %H:%M")
|
|
|
|
|
else:
|
|
|
|
|
sunrise = datetime.fromisoformat(sunrise.replace("Z", "+00:00"))
|
|
|
|
|
if isinstance(sunset, str):
|
|
|
|
|
sunset = datetime.fromisoformat(sunset.replace("Z", "+00:00"))
|
|
|
|
|
# Handle time-only format (HH:MM) by combining with today's date
|
|
|
|
|
if len(sunset) <= 5 and ":" in sunset:
|
|
|
|
|
today = datetime.now().date()
|
|
|
|
|
sunset = datetime.strptime(f"{today} {sunset}", "%Y-%m-%d %H:%M")
|
|
|
|
|
else:
|
|
|
|
|
sunset = datetime.fromisoformat(sunset.replace("Z", "+00:00"))
|
|
|
|
|
|
|
|
|
|
# Calculate daylight minutes if not provided
|
|
|
|
|
# Get daylight from various field names
|
|
|
|
|
daylight_minutes = raw_data.get("daylight_minutes") or raw_data.get("daylight")
|
|
|
|
|
if daylight_minutes is None and sunrise and sunset:
|
|
|
|
|
daylight_minutes = int((sunset - sunrise).total_seconds() / 60)
|
|
|
|
|
if daylight_minutes is None:
|
|
|
|
|
# Try to calculate from daylight_duration_seconds or daylight_hours
|
|
|
|
|
daylight_seconds = raw_data.get("daylight_duration_seconds")
|
|
|
|
|
if daylight_seconds:
|
|
|
|
|
daylight_minutes = int(daylight_seconds / 60)
|
|
|
|
|
else:
|
|
|
|
|
daylight_hours = raw_data.get("daylight_hours")
|
|
|
|
|
if daylight_hours:
|
|
|
|
|
daylight_minutes = int(daylight_hours * 60)
|
|
|
|
|
elif sunrise and sunset:
|
|
|
|
|
daylight_minutes = int((sunset - sunrise).total_seconds() / 60)
|
|
|
|
|
|
|
|
|
|
# Parse optional fields
|
|
|
|
|
solar_noon = raw_data.get("solar_noon")
|
|
|
|
@@ -167,7 +207,8 @@ class EnvironmentService:
|
|
|
|
|
if not isinstance(raw_data, dict):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
aqi = raw_data.get("aqi") or raw_data.get("index")
|
|
|
|
|
# Try various AQI field names - prefer US AQI, then European, then generic
|
|
|
|
|
aqi = raw_data.get("aqi") or raw_data.get("aqi_us") or raw_data.get("aqi_european") or raw_data.get("index")
|
|
|
|
|
if isinstance(aqi, (int, float)):
|
|
|
|
|
aqi = int(aqi)
|
|
|
|
|
|
|
|
|
@@ -177,7 +218,7 @@ class EnvironmentService:
|
|
|
|
|
pm25=raw_data.get("pm25") or raw_data.get("pm2_5"),
|
|
|
|
|
pm10=raw_data.get("pm10"),
|
|
|
|
|
o3=raw_data.get("o3") or raw_data.get("ozone"),
|
|
|
|
|
no2=raw_data.get("no2"),
|
|
|
|
|
no2=raw_data.get("no2") or raw_data.get("nitrogen_dioxide"),
|
|
|
|
|
location=raw_data.get("location"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|