Fix traffic data

This commit is contained in:
Urtzi Alfaro
2025-07-18 19:55:57 +02:00
parent 9aaa97f3fd
commit 71374dce0c
4 changed files with 669 additions and 443 deletions

View File

@@ -1,7 +1,7 @@
# ================================================================
# services/data/app/external/aemet.py
# services/data/app/external/aemet.py - FIXED VERSION
# ================================================================
"""AEMET (Spanish Weather Service) API client - PROPER API FLOW FIX"""
"""AEMET (Spanish Weather Service) API client - FIXED FORECAST PARSING"""
import math
from typing import List, Dict, Any, Optional
@@ -209,7 +209,7 @@ class AEMETClient(BaseAPIClient):
return self._get_default_weather_data()
def _parse_forecast_data(self, data: List, days: int) -> List[Dict[str, Any]]:
"""Parse AEMET forecast data"""
"""Parse AEMET forecast data - FIXED VERSION"""
forecast = []
base_date = datetime.now().date()
@@ -218,31 +218,121 @@ class AEMETClient(BaseAPIClient):
return []
try:
# AEMET forecast data structure might be different
# For now, we'll generate synthetic data based on the number of days requested
for i in range(min(days, 14)): # Limit to reasonable forecast range
forecast_date = base_date + timedelta(days=i)
# AEMET forecast structure is complex - parse what we can and fill gaps with synthetic data
logger.debug("Processing AEMET forecast data", data_length=len(data))
# If we have actual AEMET data, try to parse it
if len(data) > 0 and isinstance(data[0], dict):
aemet_data = data[0]
logger.debug("AEMET forecast keys", keys=list(aemet_data.keys()) if isinstance(aemet_data, dict) else "not_dict")
# Try to extract data from AEMET response if available
day_data = {}
if i < len(data) and isinstance(data[i], dict):
day_data = data[i]
# Try to extract daily forecasts from AEMET structure
dias = aemet_data.get('prediccion', {}).get('dia', []) if isinstance(aemet_data, dict) else []
forecast.append({
"forecast_date": datetime.combine(forecast_date, datetime.min.time()),
"generated_at": datetime.now(),
"temperature": self._safe_float(day_data.get("temperatura"), 15.0 + (i % 10)),
"precipitation": self._safe_float(day_data.get("precipitacion"), 0.0),
"humidity": self._safe_float(day_data.get("humedad"), 50.0 + (i % 20)),
"wind_speed": self._safe_float(day_data.get("viento"), 10.0 + (i % 15)),
"description": str(day_data.get("descripcion", "Partly cloudy")),
"source": "aemet"
})
except Exception as e:
logger.error("Error parsing forecast data", error=str(e))
return []
if isinstance(dias, list) and len(dias) > 0:
# Parse AEMET daily forecast format
for i, dia in enumerate(dias[:days]):
if not isinstance(dia, dict):
continue
forecast_date = base_date + timedelta(days=i)
# Extract temperature data (AEMET has complex temp structure)
temp_data = dia.get('temperatura', {})
if isinstance(temp_data, dict):
temp_max = self._extract_temp_value(temp_data.get('maxima'))
temp_min = self._extract_temp_value(temp_data.get('minima'))
avg_temp = (temp_max + temp_min) / 2 if temp_max and temp_min else 15.0
else:
avg_temp = 15.0
# Extract precipitation probability
precip_data = dia.get('probPrecipitacion', [])
precip_prob = 0.0
if isinstance(precip_data, list) and len(precip_data) > 0:
for precip_item in precip_data:
if isinstance(precip_item, dict) and 'value' in precip_item:
precip_prob = max(precip_prob, self._safe_float(precip_item.get('value'), 0.0))
# Extract wind data
viento_data = dia.get('viento', [])
wind_speed = 10.0
if isinstance(viento_data, list) and len(viento_data) > 0:
for viento_item in viento_data:
if isinstance(viento_item, dict) and 'velocidad' in viento_item:
speed_values = viento_item.get('velocidad', [])
if isinstance(speed_values, list) and len(speed_values) > 0:
wind_speed = self._safe_float(speed_values[0], 10.0)
break
# Generate description based on precipitation probability
if precip_prob > 70:
description = "Lluvioso"
elif precip_prob > 30:
description = "Parcialmente nublado"
else:
description = "Soleado"
forecast.append({
"forecast_date": datetime.combine(forecast_date, datetime.min.time()),
"generated_at": datetime.now(),
"temperature": round(avg_temp, 1),
"precipitation": precip_prob / 10, # Convert percentage to mm estimate
"humidity": 50.0 + (i % 20), # Estimate
"wind_speed": round(wind_speed, 1),
"description": description,
"source": "aemet"
})
logger.debug("Parsed forecast day", day=i, temp=avg_temp, precip=precip_prob)
# If we successfully parsed some days, fill remaining with synthetic
remaining_days = days - len(forecast)
if remaining_days > 0:
synthetic_forecast = self._generate_synthetic_forecast_sync(remaining_days, len(forecast))
forecast.extend(synthetic_forecast)
# If no valid AEMET data was parsed, use synthetic
if len(forecast) == 0:
logger.info("No valid AEMET forecast data found, using synthetic")
forecast = self._generate_synthetic_forecast_sync(days, 0)
return forecast
except Exception as e:
logger.error("Error parsing AEMET forecast data", error=str(e))
# Fallback to synthetic forecast
forecast = self._generate_synthetic_forecast_sync(days, 0)
# Ensure we always return the requested number of days
if len(forecast) < days:
remaining = days - len(forecast)
synthetic_remaining = self._generate_synthetic_forecast_sync(remaining, len(forecast))
forecast.extend(synthetic_remaining)
return forecast[:days] # Ensure we don't exceed requested days
def _extract_temp_value(self, temp_data) -> Optional[float]:
"""Extract temperature value from AEMET complex temperature structure"""
if temp_data is None:
return None
if isinstance(temp_data, (int, float)):
return float(temp_data)
if isinstance(temp_data, str):
try:
return float(temp_data)
except ValueError:
return None
if isinstance(temp_data, dict) and 'valor' in temp_data:
return self._safe_float(temp_data['valor'], None)
if isinstance(temp_data, list) and len(temp_data) > 0:
first_item = temp_data[0]
if isinstance(first_item, dict) and 'valor' in first_item:
return self._safe_float(first_item['valor'], None)
return None
def _safe_float(self, value: Any, default: float) -> float:
"""Safely convert value to float with fallback"""
@@ -292,32 +382,36 @@ class AEMETClient(BaseAPIClient):
"source": "synthetic"
}
async def _generate_synthetic_forecast(self, days: int) -> List[Dict[str, Any]]:
"""Generate synthetic forecast data"""
def _generate_synthetic_forecast_sync(self, days: int, start_offset: int = 0) -> List[Dict[str, Any]]:
"""Generate synthetic forecast data synchronously"""
forecast = []
base_date = datetime.now().date()
for i in range(days):
forecast_date = base_date + timedelta(days=i)
forecast_date = base_date + timedelta(days=start_offset + i)
# Seasonal temperature
month = forecast_date.month
base_temp = 5 + (month - 1) * 2.5
temp_variation = (i % 7 - 3) * 2 # Weekly variation
temp_variation = ((start_offset + i) % 7 - 3) * 2 # Weekly variation
forecast.append({
"forecast_date": datetime.combine(forecast_date, datetime.min.time()),
"generated_at": datetime.now(),
"temperature": round(base_temp + temp_variation, 1),
"precipitation": 2.0 if i % 5 == 0 else 0.0,
"humidity": 50 + (i % 30),
"wind_speed": 10 + (i % 15),
"description": "Lluvioso" if i % 5 == 0 else "Soleado",
"precipitation": 2.0 if (start_offset + i) % 5 == 0 else 0.0,
"humidity": 50 + ((start_offset + i) % 30),
"wind_speed": 10 + ((start_offset + i) % 15),
"description": "Lluvioso" if (start_offset + i) % 5 == 0 else "Soleado",
"source": "synthetic"
})
return forecast
async def _generate_synthetic_forecast(self, days: int) -> List[Dict[str, Any]]:
"""Generate synthetic forecast data (async version for compatibility)"""
return self._generate_synthetic_forecast_sync(days, 0)
async def _generate_synthetic_historical(self, start_date: datetime, end_date: datetime) -> List[Dict[str, Any]]:
"""Generate synthetic historical weather data"""
historical_data = []