Improve the dahboard 5
This commit is contained in:
181
services/external/app/external/aemet.py
vendored
181
services/external/app/external/aemet.py
vendored
@@ -136,16 +136,36 @@ class WeatherDataParser:
|
|||||||
return self._get_default_weather_data()
|
return self._get_default_weather_data()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return {
|
# Log the raw data to understand the structure
|
||||||
|
logger.debug("Parsing current weather data", data_keys=list(data.keys()) if isinstance(data, dict) else "not_dict")
|
||||||
|
|
||||||
|
# Enhanced parsing for AEMET station data
|
||||||
|
result = {
|
||||||
"date": datetime.now(),
|
"date": datetime.now(),
|
||||||
"temperature": self.safe_float(data.get("ta"), 15.0),
|
"temperature": self.safe_float(data.get("ta"), self.safe_float(data.get("temperature"))),
|
||||||
"precipitation": self.safe_float(data.get("prec"), 0.0),
|
"precipitation": self.safe_float(data.get("prec"), self.safe_float(data.get("precipitation"), 0.0)),
|
||||||
"humidity": self.safe_float(data.get("hr"), 50.0),
|
"humidity": self.safe_float(data.get("hr"), self.safe_float(data.get("humidity"))),
|
||||||
"wind_speed": self.safe_float(data.get("vv"), 10.0),
|
"wind_speed": self.safe_float(data.get("vv"), self.safe_float(data.get("wind_speed"))),
|
||||||
"pressure": self.safe_float(data.get("pres"), 1013.0),
|
"pressure": self.safe_float(data.get("pres"), self.safe_float(data.get("pressure"))),
|
||||||
"description": str(data.get("descripcion", "Partly cloudy")),
|
"description": str(data.get("descripcion", data.get("description", "Partly cloudy"))),
|
||||||
"source": WeatherSource.AEMET.value
|
"source": WeatherSource.AEMET.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Set fallback values for required fields that are None
|
||||||
|
if result["temperature"] is None:
|
||||||
|
result["temperature"] = 15.0
|
||||||
|
if result["humidity"] is None:
|
||||||
|
result["humidity"] = 50.0
|
||||||
|
if result["wind_speed"] is None:
|
||||||
|
result["wind_speed"] = 10.0
|
||||||
|
if result["pressure"] is None:
|
||||||
|
result["pressure"] = 1013.0
|
||||||
|
|
||||||
|
logger.debug("Parsed current weather successfully",
|
||||||
|
temperature=result["temperature"],
|
||||||
|
source=result["source"])
|
||||||
|
|
||||||
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error parsing weather data", error=str(e), data=data)
|
logger.error("Error parsing weather data", error=str(e), data=data)
|
||||||
return self._get_default_weather_data()
|
return self._get_default_weather_data()
|
||||||
@@ -204,22 +224,29 @@ class WeatherDataParser:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# AEMET hourly forecast has different structure than daily
|
||||||
|
logger.debug("Processing AEMET hourly forecast data", data_length=len(data))
|
||||||
|
|
||||||
if len(data) > 0 and isinstance(data[0], dict):
|
if len(data) > 0 and isinstance(data[0], dict):
|
||||||
aemet_data = data[0]
|
aemet_data = data[0]
|
||||||
prediccion = aemet_data.get('prediccion', {})
|
prediccion = aemet_data.get('prediccion', {})
|
||||||
|
|
||||||
|
# AEMET hourly structure: prediccion -> dia[] -> each day has hourly arrays
|
||||||
dias = prediccion.get('dia', [])
|
dias = prediccion.get('dia', [])
|
||||||
|
|
||||||
if isinstance(dias, list):
|
if isinstance(dias, list):
|
||||||
hourly_forecast = self._parse_hourly_forecast_days(dias, hours, base_datetime)
|
hourly_forecast = self._parse_real_hourly_forecast_days(dias, hours, base_datetime)
|
||||||
|
logger.info("Successfully parsed AEMET hourly forecast", count=len(hourly_forecast))
|
||||||
|
|
||||||
# Fill remaining hours with synthetic data if needed
|
if len(hourly_forecast) == 0:
|
||||||
hourly_forecast = self._ensure_hourly_forecast_completeness(hourly_forecast, hours)
|
logger.warning("No hourly data parsed from AEMET response")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error parsing AEMET hourly forecast data", error=str(e))
|
logger.error("Error parsing AEMET hourly forecast data", error=str(e))
|
||||||
hourly_forecast = []
|
hourly_forecast = []
|
||||||
|
|
||||||
return hourly_forecast
|
return hourly_forecast[:hours]
|
||||||
|
|
||||||
|
|
||||||
def _parse_single_historical_record(self, record: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
def _parse_single_historical_record(self, record: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
"""Parse a single historical weather record"""
|
"""Parse a single historical weather record"""
|
||||||
@@ -351,8 +378,121 @@ class WeatherDataParser:
|
|||||||
else:
|
else:
|
||||||
return "Soleado"
|
return "Soleado"
|
||||||
|
|
||||||
|
def _parse_real_hourly_forecast_days(self, dias: List[Dict[str, Any]], hours: int, base_datetime: datetime) -> List[Dict[str, Any]]:
|
||||||
|
"""Parse real AEMET hourly forecast days"""
|
||||||
|
hourly_forecast = []
|
||||||
|
current_hour = 0
|
||||||
|
|
||||||
|
for day_index, day_data in enumerate(dias):
|
||||||
|
if current_hour >= hours:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not isinstance(day_data, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse hourly data from this day using real AEMET structure
|
||||||
|
day_hourly = self._parse_real_aemet_hourly_day(day_data, base_datetime, day_index, current_hour, hours)
|
||||||
|
hourly_forecast.extend(day_hourly)
|
||||||
|
current_hour += len(day_hourly)
|
||||||
|
|
||||||
|
return hourly_forecast[:hours]
|
||||||
|
|
||||||
|
def _parse_real_aemet_hourly_day(self, day_data: Dict[str, Any], base_datetime: datetime, day_index: int, start_hour: int, max_hours: int) -> List[Dict[str, Any]]:
|
||||||
|
"""Parse hourly data for a single day from real AEMET hourly API response"""
|
||||||
|
hourly_data = []
|
||||||
|
|
||||||
|
# Extract fecha (date) for this day
|
||||||
|
fecha_str = day_data.get('fecha', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if fecha_str:
|
||||||
|
day_date = datetime.strptime(fecha_str, '%Y-%m-%d')
|
||||||
|
else:
|
||||||
|
day_date = base_datetime.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=day_index)
|
||||||
|
except ValueError:
|
||||||
|
day_date = base_datetime.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=day_index)
|
||||||
|
|
||||||
|
# Extract hourly arrays from AEMET structure
|
||||||
|
temperatura = day_data.get('temperatura', [])
|
||||||
|
precipitacion = day_data.get('precipitacion', [])
|
||||||
|
humedadRelativa = day_data.get('humedadRelativa', [])
|
||||||
|
viento = day_data.get('viento', [])
|
||||||
|
|
||||||
|
# Process available hourly data
|
||||||
|
max_available_hours = min(
|
||||||
|
len(temperatura) if isinstance(temperatura, list) else 0,
|
||||||
|
len(precipitacion) if isinstance(precipitacion, list) else 0,
|
||||||
|
24, # Max 24 hours per day
|
||||||
|
max_hours - start_hour
|
||||||
|
)
|
||||||
|
|
||||||
|
if max_available_hours == 0:
|
||||||
|
logger.warning("No hourly data arrays found in day data", fecha=fecha_str)
|
||||||
|
return []
|
||||||
|
|
||||||
|
for hour_idx in range(max_available_hours):
|
||||||
|
forecast_datetime = day_date + timedelta(hours=hour_idx)
|
||||||
|
|
||||||
|
# Extract hourly values safely
|
||||||
|
temp = self._extract_aemet_hourly_value(temperatura, hour_idx)
|
||||||
|
precip = self._extract_aemet_hourly_value(precipitacion, hour_idx, default=0.0)
|
||||||
|
humidity = self._extract_aemet_hourly_value(humedadRelativa, hour_idx)
|
||||||
|
wind_data = viento[hour_idx] if isinstance(viento, list) and hour_idx < len(viento) else {}
|
||||||
|
wind_speed = self._extract_wind_from_aemet_data(wind_data)
|
||||||
|
|
||||||
|
hourly_data.append({
|
||||||
|
"forecast_datetime": forecast_datetime,
|
||||||
|
"generated_at": datetime.now(),
|
||||||
|
"temperature": temp if temp is not None else 15.0,
|
||||||
|
"precipitation": precip if precip is not None else 0.0,
|
||||||
|
"humidity": humidity if humidity is not None else 50.0,
|
||||||
|
"wind_speed": wind_speed if wind_speed is not None else 10.0,
|
||||||
|
"description": self._generate_hourly_description(temp, precip),
|
||||||
|
"source": WeatherSource.AEMET.value,
|
||||||
|
"hour": forecast_datetime.hour
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.debug("Parsed AEMET hourly day", fecha=fecha_str, hours_parsed=len(hourly_data))
|
||||||
|
return hourly_data
|
||||||
|
|
||||||
|
def _extract_aemet_hourly_value(self, data_array: Any, hour_idx: int, default: Optional[float] = None) -> Optional[float]:
|
||||||
|
"""Extract hourly value from AEMET hourly data array"""
|
||||||
|
if not isinstance(data_array, list) or hour_idx >= len(data_array):
|
||||||
|
return default
|
||||||
|
|
||||||
|
value_data = data_array[hour_idx]
|
||||||
|
|
||||||
|
# Handle different AEMET data formats
|
||||||
|
if isinstance(value_data, (int, float)):
|
||||||
|
return float(value_data)
|
||||||
|
elif isinstance(value_data, dict):
|
||||||
|
# Try common AEMET field names
|
||||||
|
for field in ['value', 'valor', 'v']:
|
||||||
|
if field in value_data:
|
||||||
|
return self.safe_float(value_data[field], default)
|
||||||
|
elif isinstance(value_data, str):
|
||||||
|
return self.safe_float(value_data, default)
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
def _extract_wind_from_aemet_data(self, wind_data: Any) -> Optional[float]:
|
||||||
|
"""Extract wind speed from AEMET wind data structure"""
|
||||||
|
if isinstance(wind_data, dict):
|
||||||
|
# Try common wind speed fields
|
||||||
|
for field in ['velocidad', 'speed', 'v']:
|
||||||
|
if field in wind_data:
|
||||||
|
velocidad = wind_data[field]
|
||||||
|
if isinstance(velocidad, list) and len(velocidad) > 0:
|
||||||
|
return self.safe_float(velocidad[0])
|
||||||
|
else:
|
||||||
|
return self.safe_float(velocidad)
|
||||||
|
elif isinstance(wind_data, (int, float)):
|
||||||
|
return float(wind_data)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def _parse_hourly_forecast_days(self, dias: List[Dict[str, Any]], hours: int, base_datetime: datetime) -> List[Dict[str, Any]]:
|
def _parse_hourly_forecast_days(self, dias: List[Dict[str, Any]], hours: int, base_datetime: datetime) -> List[Dict[str, Any]]:
|
||||||
"""Parse hourly forecast days from AEMET data"""
|
"""Parse hourly forecast days from AEMET data (legacy method)"""
|
||||||
hourly_forecast = []
|
hourly_forecast = []
|
||||||
current_hour = 0
|
current_hour = 0
|
||||||
|
|
||||||
@@ -453,6 +593,7 @@ class WeatherDataParser:
|
|||||||
"""Return forecast as is - no synthetic data filling"""
|
"""Return forecast as is - no synthetic data filling"""
|
||||||
return forecast[:days]
|
return forecast[:days]
|
||||||
|
|
||||||
|
|
||||||
def _ensure_hourly_forecast_completeness(self, forecast: List[Dict[str, Any]], hours: int) -> List[Dict[str, Any]]:
|
def _ensure_hourly_forecast_completeness(self, forecast: List[Dict[str, Any]], hours: int) -> List[Dict[str, Any]]:
|
||||||
"""Return hourly forecast as is - no synthetic data filling"""
|
"""Return hourly forecast as is - no synthetic data filling"""
|
||||||
return forecast[:hours]
|
return forecast[:hours]
|
||||||
@@ -630,19 +771,20 @@ class AEMETClient(BaseAPIClient):
|
|||||||
|
|
||||||
logger.info("✅ Found municipality code", municipality_code=municipality_code)
|
logger.info("✅ Found municipality code", municipality_code=municipality_code)
|
||||||
|
|
||||||
|
# Get real hourly data from AEMET API
|
||||||
hourly_data = await self._fetch_hourly_forecast_data(municipality_code)
|
hourly_data = await self._fetch_hourly_forecast_data(municipality_code)
|
||||||
if hourly_data:
|
if hourly_data:
|
||||||
logger.info("🎉 SUCCESS: Real AEMET hourly data retrieved!", municipality_code=municipality_code)
|
logger.info("🎉 SUCCESS: Real AEMET hourly data retrieved!", municipality_code=municipality_code)
|
||||||
parsed_data = self.parser.parse_hourly_forecast_data(hourly_data, hours)
|
parsed_data = self.parser.parse_hourly_forecast_data(hourly_data, hours)
|
||||||
if parsed_data:
|
if parsed_data and len(parsed_data) > 0:
|
||||||
return parsed_data
|
return parsed_data
|
||||||
|
|
||||||
logger.error("⚠️ AEMET hourly API connectivity issues",
|
logger.error("⚠️ AEMET hourly API failed or returned no data",
|
||||||
municipality_code=municipality_code, reason="aemet_hourly_api_unreachable")
|
municipality_code=municipality_code)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("❌ AEMET hourly API failed",
|
logger.error("❌ AEMET hourly forecast failed",
|
||||||
error=str(e), error_type=type(e).__name__)
|
error=str(e), error_type=type(e).__name__)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -709,13 +851,20 @@ class AEMETClient(BaseAPIClient):
|
|||||||
|
|
||||||
async def _fetch_hourly_forecast_data(self, municipality_code: str) -> Optional[List[Dict[str, Any]]]:
|
async def _fetch_hourly_forecast_data(self, municipality_code: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
"""Fetch hourly forecast data from AEMET API"""
|
"""Fetch hourly forecast data from AEMET API"""
|
||||||
|
# Note: AEMET hourly forecast API endpoint
|
||||||
endpoint = f"/prediccion/especifica/municipio/horaria/{municipality_code}"
|
endpoint = f"/prediccion/especifica/municipio/horaria/{municipality_code}"
|
||||||
|
logger.info("Requesting AEMET hourly forecast", endpoint=endpoint, municipality=municipality_code)
|
||||||
|
|
||||||
initial_response = await self._get(endpoint)
|
initial_response = await self._get(endpoint)
|
||||||
|
|
||||||
if not self._is_valid_initial_response(initial_response):
|
if not self._is_valid_initial_response(initial_response):
|
||||||
|
logger.warning("Invalid initial response from AEMET hourly API",
|
||||||
|
response=initial_response, municipality=municipality_code)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
datos_url = initial_response.get("datos")
|
datos_url = initial_response.get("datos")
|
||||||
|
logger.info("Fetching hourly data from AEMET datos URL", url=datos_url)
|
||||||
|
|
||||||
return await self._fetch_from_url(datos_url)
|
return await self._fetch_from_url(datos_url)
|
||||||
|
|
||||||
async def _fetch_historical_data_in_chunks(self,
|
async def _fetch_historical_data_in_chunks(self,
|
||||||
|
|||||||
Reference in New Issue
Block a user