diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f7c8e..1a68ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.10.10] - 2026-01-08 + +### Fixed + +- **Environment data parsing** - Fix parsing of scheduler-generated Qdrant data + - Forecast: Check `daily` key first (scheduler stores day count in `days`, list in `daily`) + - Sun times: Use `sunrise_iso`/`sunset_iso` fields, handle time-only format fallback + - Sun times: Calculate daylight from `daylight_duration_seconds` or `daylight_hours` + - Air quality: Support `aqi_us`/`aqi_european` and `nitrogen_dioxide` field names + ## [1.10.9] - 2026-01-08 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 68392e2..e9501f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "core-api" -version = "1.10.9" +version = "1.10.10" description = "Core Code API - Infrastructure management and tools API" readme = "README.md" requires-python = ">=3.12" diff --git a/src/domains/tools/environment/service.py b/src/domains/tools/environment/service.py index 0e23a78..9aab2b8 100644 --- a/src/domains/tools/environment/service.py +++ b/src/domains/tools/environment/service.py @@ -73,7 +73,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 +91,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 +114,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 +195,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 +206,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"), )