Files
bakery-ia/services/data/app/external/aemet.py

255 lines
10 KiB
Python
Raw Normal View History

2025-07-18 11:51:43 +02:00
# ================================================================
# services/data/app/external/aemet.py
# ================================================================
"""AEMET (Spanish Weather Service) API client"""
import math
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
import structlog
from app.external.base_client import BaseAPIClient
from app.core.config import settings
logger = structlog.get_logger()
class AEMETClient(BaseAPIClient):
def __init__(self):
super().__init__(
base_url="https://opendata.aemet.es/opendata/api",
api_key=settings.AEMET_API_KEY
)
async def get_current_weather(self, latitude: float, longitude: float) -> Optional[Dict[str, Any]]:
"""Get current weather for coordinates"""
try:
# Find nearest station
station_id = await self._get_nearest_station(latitude, longitude)
if not station_id:
logger.warning("No weather station found", lat=latitude, lon=longitude)
return await self._generate_synthetic_weather()
# Get current weather from station
endpoint = f"/observacion/convencional/datos/estacion/{station_id}"
response = await self._get(endpoint)
if response and response.get("datos"):
# Parse AEMET response
weather_data = response["datos"][0] if response["datos"] else {}
return self._parse_weather_data(weather_data)
# Fallback to synthetic data
return await self._generate_synthetic_weather()
except Exception as e:
logger.error("Failed to get current weather", error=str(e))
return await self._generate_synthetic_weather()
async def get_forecast(self, latitude: float, longitude: float, days: int = 7) -> List[Dict[str, Any]]:
"""Get weather forecast for coordinates"""
try:
# Get municipality code for location
municipality_code = await self._get_municipality_code(latitude, longitude)
if not municipality_code:
return await self._generate_synthetic_forecast(days)
# Get forecast
endpoint = f"/prediccion/especifica/municipio/diaria/{municipality_code}"
response = await self._get(endpoint)
if response and response.get("datos"):
return self._parse_forecast_data(response["datos"], days)
# Fallback to synthetic data
return await self._generate_synthetic_forecast(days)
except Exception as e:
logger.error("Failed to get weather forecast", error=str(e))
return await self._generate_synthetic_forecast(days)
async def get_historical_weather(self,
latitude: float,
longitude: float,
start_date: datetime,
end_date: datetime) -> List[Dict[str, Any]]:
"""Get historical weather data"""
try:
# For now, generate synthetic historical data
# In production, this would use AEMET historical data API
return await self._generate_synthetic_historical(start_date, end_date)
except Exception as e:
logger.error("Failed to get historical weather", error=str(e))
return []
async def _get_nearest_station(self, latitude: float, longitude: float) -> Optional[str]:
"""Find nearest weather station"""
try:
# Madrid area stations (simplified)
madrid_stations = {
"3195": {"lat": 40.4168, "lon": -3.7038, "name": "Madrid Centro"},
"3196": {"lat": 40.4518, "lon": -3.7246, "name": "Madrid Norte"},
"3197": {"lat": 40.3833, "lon": -3.7167, "name": "Madrid Sur"}
}
closest_station = None
min_distance = float('inf')
for station_id, station_data in madrid_stations.items():
distance = self._calculate_distance(
latitude, longitude,
station_data["lat"], station_data["lon"]
)
if distance < min_distance:
min_distance = distance
closest_station = station_id
return closest_station
except Exception as e:
logger.error("Failed to find nearest station", error=str(e))
return None
async def _get_municipality_code(self, latitude: float, longitude: float) -> Optional[str]:
"""Get municipality code for coordinates"""
# Madrid municipality code
if self._is_in_madrid_area(latitude, longitude):
return "28079" # Madrid municipality code
return None
def _is_in_madrid_area(self, latitude: float, longitude: float) -> bool:
"""Check if coordinates are in Madrid area"""
# Madrid approximate bounds
return (40.3 <= latitude <= 40.6) and (-3.9 <= longitude <= -3.5)
def _calculate_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""Calculate distance between two coordinates in km"""
R = 6371 # Earth's radius in km
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat/2) * math.sin(dlat/2) +
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
math.sin(dlon/2) * math.sin(dlon/2))
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
distance = R * c
return distance
def _parse_weather_data(self, data: Dict) -> Dict[str, Any]:
"""Parse AEMET weather data format"""
return {
"date": datetime.now(),
"temperature": data.get("ta", 15.0), # Temperature
"precipitation": data.get("prec", 0.0), # Precipitation
"humidity": data.get("hr", 50.0), # Humidity
"wind_speed": data.get("vv", 10.0), # Wind speed
"pressure": data.get("pres", 1013.0), # Pressure
"description": "Partly cloudy",
"source": "aemet"
}
def _parse_forecast_data(self, data: List, days: int) -> List[Dict[str, Any]]:
"""Parse AEMET forecast data"""
forecast = []
base_date = datetime.now().date()
for i in range(min(days, len(data))):
forecast_date = base_date + timedelta(days=i)
day_data = data[i] if i < len(data) else {}
forecast.append({
"forecast_date": datetime.combine(forecast_date, datetime.min.time()),
"generated_at": datetime.now(),
"temperature": day_data.get("temperatura", 15.0),
"precipitation": day_data.get("precipitacion", 0.0),
"humidity": day_data.get("humedad", 50.0),
"wind_speed": day_data.get("viento", 10.0),
"description": day_data.get("descripcion", "Partly cloudy"),
"source": "aemet"
})
return forecast
async def _generate_synthetic_weather(self) -> Dict[str, Any]:
"""Generate realistic synthetic weather for Madrid"""
now = datetime.now()
month = now.month
hour = now.hour
# Madrid climate simulation
base_temp = 5 + (month - 1) * 2.5 # Seasonal variation
temp_variation = math.sin((hour - 6) * math.pi / 12) * 8 # Daily variation
temperature = base_temp + temp_variation
# Rain probability (higher in winter)
rain_prob = 0.3 if month in [11, 12, 1, 2, 3] else 0.1
precipitation = 2.5 if hash(now.date()) % 100 < rain_prob * 100 else 0.0
return {
"date": now,
"temperature": round(temperature, 1),
"precipitation": precipitation,
"humidity": 45 + (month % 6) * 5,
"wind_speed": 8 + (hour % 12),
"pressure": 1013 + math.sin(now.day * 0.2) * 15,
"description": "Lluvioso" if precipitation > 0 else "Soleado",
"source": "synthetic"
}
async def _generate_synthetic_forecast(self, days: int) -> List[Dict[str, Any]]:
"""Generate synthetic forecast data"""
forecast = []
base_date = datetime.now().date()
for i in range(days):
forecast_date = base_date + timedelta(days=i)
# Seasonal temperature
month = forecast_date.month
base_temp = 5 + (month - 1) * 2.5
temp_variation = (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",
"source": "synthetic"
})
return forecast
async def _generate_synthetic_historical(self, start_date: datetime, end_date: datetime) -> List[Dict[str, Any]]:
"""Generate synthetic historical weather data"""
historical_data = []
current_date = start_date
while current_date <= end_date:
month = current_date.month
base_temp = 5 + (month - 1) * 2.5
# Add some randomness based on date
temp_variation = math.sin(current_date.day * 0.3) * 5
historical_data.append({
"date": current_date,
"temperature": round(base_temp + temp_variation, 1),
"precipitation": 1.5 if current_date.day % 7 == 0 else 0.0,
"humidity": 45 + (current_date.day % 40),
"wind_speed": 8 + (current_date.day % 20),
"pressure": 1013 + math.sin(current_date.day * 0.2) * 20,
"description": "Variable",
"source": "synthetic"
})
current_date += timedelta(days=1)
return historical_data