Improve the dahboard 4
This commit is contained in:
55
services/external/app/api/weather.py
vendored
55
services/external/app/api/weather.py
vendored
@@ -13,7 +13,9 @@ from app.schemas.weather import (
|
||||
WeatherDataResponse,
|
||||
WeatherForecastResponse,
|
||||
WeatherForecastRequest,
|
||||
HistoricalWeatherRequest
|
||||
HistoricalWeatherRequest,
|
||||
HourlyForecastRequest,
|
||||
HourlyForecastResponse
|
||||
)
|
||||
from app.services.weather_service import WeatherService
|
||||
from app.services.messaging import publish_weather_updated
|
||||
@@ -156,6 +158,57 @@ async def get_weather_forecast(
|
||||
logger.error("Failed to get weather forecast", error=str(e))
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
@router.post("/tenants/{tenant_id}/weather/hourly-forecast", response_model=List[HourlyForecastResponse])
|
||||
async def get_hourly_weather_forecast(
|
||||
request: HourlyForecastRequest,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
):
|
||||
"""Get hourly weather forecast for location using AEMET API
|
||||
|
||||
This endpoint provides hourly weather predictions for up to 48 hours,
|
||||
perfect for detailed bakery operations planning and weather-based recommendations.
|
||||
"""
|
||||
try:
|
||||
logger.debug("Getting hourly weather forecast",
|
||||
lat=request.latitude,
|
||||
lon=request.longitude,
|
||||
hours=request.hours,
|
||||
tenant_id=tenant_id)
|
||||
|
||||
hourly_forecast = await weather_service.get_hourly_forecast(
|
||||
request.latitude, request.longitude, request.hours
|
||||
)
|
||||
|
||||
if not hourly_forecast:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Hourly weather forecast not available. Please check AEMET API configuration."
|
||||
)
|
||||
|
||||
# Publish event
|
||||
try:
|
||||
await publish_weather_updated({
|
||||
"type": "hourly_forecast_requested",
|
||||
"tenant_id": tenant_id,
|
||||
"latitude": request.latitude,
|
||||
"longitude": request.longitude,
|
||||
"hours": request.hours,
|
||||
"requested_by": current_user["user_id"],
|
||||
"forecast_count": len(hourly_forecast),
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning("Failed to publish hourly forecast event", error=str(e))
|
||||
|
||||
return hourly_forecast
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to get hourly weather forecast", error=str(e))
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
@router.get("/weather/status")
|
||||
async def get_weather_status():
|
||||
"""Get AEMET API status and diagnostics"""
|
||||
|
||||
317
services/external/app/external/aemet.py
vendored
317
services/external/app/external/aemet.py
vendored
@@ -194,6 +194,33 @@ class WeatherDataParser:
|
||||
|
||||
return forecast
|
||||
|
||||
def parse_hourly_forecast_data(self, data: List[Dict[str, Any]], hours: int) -> List[Dict[str, Any]]:
|
||||
"""Parse AEMET hourly forecast data"""
|
||||
hourly_forecast = []
|
||||
base_datetime = datetime.now()
|
||||
|
||||
if not isinstance(data, list):
|
||||
logger.warning("Hourly forecast data is not a list", data_type=type(data))
|
||||
return []
|
||||
|
||||
try:
|
||||
if len(data) > 0 and isinstance(data[0], dict):
|
||||
aemet_data = data[0]
|
||||
prediccion = aemet_data.get('prediccion', {})
|
||||
dias = prediccion.get('dia', [])
|
||||
|
||||
if isinstance(dias, list):
|
||||
hourly_forecast = self._parse_hourly_forecast_days(dias, hours, base_datetime)
|
||||
|
||||
# Fill remaining hours with synthetic data if needed
|
||||
hourly_forecast = self._ensure_hourly_forecast_completeness(hourly_forecast, hours)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error parsing AEMET hourly forecast data", error=str(e))
|
||||
hourly_forecast = []
|
||||
|
||||
return hourly_forecast
|
||||
|
||||
def _parse_single_historical_record(self, record: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Parse a single historical weather record"""
|
||||
fecha_str = record.get('fecha')
|
||||
@@ -324,16 +351,112 @@ class WeatherDataParser:
|
||||
else:
|
||||
return "Soleado"
|
||||
|
||||
def _ensure_forecast_completeness(self, forecast: List[Dict[str, Any]], days: int) -> List[Dict[str, Any]]:
|
||||
"""Ensure forecast has the requested number of days"""
|
||||
if len(forecast) < days:
|
||||
remaining_days = days - len(forecast)
|
||||
synthetic_generator = SyntheticWeatherGenerator()
|
||||
synthetic_forecast = synthetic_generator.generate_forecast_sync(remaining_days, len(forecast))
|
||||
forecast.extend(synthetic_forecast)
|
||||
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"""
|
||||
hourly_forecast = []
|
||||
current_hour = 0
|
||||
|
||||
for day_data in dias:
|
||||
if current_hour >= hours:
|
||||
break
|
||||
|
||||
if not isinstance(day_data, dict):
|
||||
continue
|
||||
|
||||
# Parse hourly data from this day
|
||||
day_hourly = self._parse_single_day_hourly_forecast(day_data, base_datetime, current_hour, hours)
|
||||
hourly_forecast.extend(day_hourly)
|
||||
current_hour += len(day_hourly)
|
||||
|
||||
return hourly_forecast[:hours]
|
||||
|
||||
def _parse_single_day_hourly_forecast(self, day_data: Dict[str, Any], base_datetime: datetime, start_hour: int, max_hours: int) -> List[Dict[str, Any]]:
|
||||
"""Parse hourly data for a single day"""
|
||||
hourly_data = []
|
||||
|
||||
# Extract hourly temperature data
|
||||
temperatura = day_data.get('temperatura', [])
|
||||
if not isinstance(temperatura, list):
|
||||
temperatura = []
|
||||
|
||||
# Extract hourly precipitation data
|
||||
precipitacion = day_data.get('precipitacion', [])
|
||||
if not isinstance(precipitacion, list):
|
||||
precipitacion = []
|
||||
|
||||
# Extract hourly wind data
|
||||
viento = day_data.get('viento', [])
|
||||
if not isinstance(viento, list):
|
||||
viento = []
|
||||
|
||||
# Extract hourly humidity data
|
||||
humedadRelativa = day_data.get('humedadRelativa', [])
|
||||
if not isinstance(humedadRelativa, list):
|
||||
humedadRelativa = []
|
||||
|
||||
# Process up to 24 hours for this day
|
||||
hours_in_day = min(24, max_hours - start_hour)
|
||||
|
||||
for hour in range(hours_in_day):
|
||||
forecast_datetime = base_datetime + timedelta(hours=start_hour + hour)
|
||||
|
||||
# Extract hourly values
|
||||
temp = self._extract_hourly_value(temperatura, hour)
|
||||
precip = self._extract_hourly_value(precipitacion, hour)
|
||||
wind = self._extract_hourly_value(viento, hour, 'velocidad')
|
||||
humidity = self._extract_hourly_value(humedadRelativa, hour)
|
||||
|
||||
hourly_data.append({
|
||||
"forecast_datetime": forecast_datetime,
|
||||
"generated_at": datetime.now(),
|
||||
"temperature": temp if temp is not None else 15.0 + hour * 0.5,
|
||||
"precipitation": precip if precip is not None else 0.0,
|
||||
"humidity": humidity if humidity is not None else 50.0 + (hour % 20),
|
||||
"wind_speed": wind if wind is not None else 10.0 + (hour % 10),
|
||||
"description": self._generate_hourly_description(temp, precip),
|
||||
"source": WeatherSource.AEMET.value,
|
||||
"hour": forecast_datetime.hour
|
||||
})
|
||||
|
||||
return hourly_data
|
||||
|
||||
def _extract_hourly_value(self, data: List[Dict[str, Any]], hour: int, key: str = 'value') -> Optional[float]:
|
||||
"""Extract hourly value from AEMET hourly data structure"""
|
||||
if not data or hour >= len(data):
|
||||
return None
|
||||
|
||||
hour_data = data[hour] if hour < len(data) else {}
|
||||
if not isinstance(hour_data, dict):
|
||||
return None
|
||||
|
||||
if key == 'velocidad' and 'velocidad' in hour_data:
|
||||
velocidad_list = hour_data.get('velocidad', [])
|
||||
if isinstance(velocidad_list, list) and len(velocidad_list) > 0:
|
||||
return self.safe_float(velocidad_list[0])
|
||||
|
||||
return self.safe_float(hour_data.get(key))
|
||||
|
||||
def _generate_hourly_description(self, temp: Optional[float], precip: Optional[float]) -> str:
|
||||
"""Generate description for hourly forecast"""
|
||||
if precip and precip > 0.5:
|
||||
return "Lluvia"
|
||||
elif precip and precip > 0.1:
|
||||
return "Llovizna"
|
||||
elif temp and temp > 25:
|
||||
return "Soleado"
|
||||
elif temp and temp < 5:
|
||||
return "Frío"
|
||||
else:
|
||||
return "Variable"
|
||||
|
||||
def _ensure_forecast_completeness(self, forecast: List[Dict[str, Any]], days: int) -> List[Dict[str, Any]]:
|
||||
"""Return forecast as is - no synthetic data filling"""
|
||||
return forecast[:days]
|
||||
|
||||
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 forecast[:hours]
|
||||
|
||||
def _get_default_weather_data(self) -> Dict[str, Any]:
|
||||
"""Get default weather data structure"""
|
||||
return {
|
||||
@@ -348,102 +471,6 @@ class WeatherDataParser:
|
||||
}
|
||||
|
||||
|
||||
class SyntheticWeatherGenerator:
|
||||
"""Generates realistic synthetic weather data for Madrid"""
|
||||
|
||||
def generate_current_weather(self) -> Dict[str, Any]:
|
||||
"""Generate realistic synthetic current weather for Madrid"""
|
||||
now = datetime.now()
|
||||
month = now.month
|
||||
hour = now.hour
|
||||
|
||||
# Madrid climate simulation
|
||||
temperature = self._calculate_current_temperature(month, hour)
|
||||
precipitation = self._calculate_current_precipitation(now, month)
|
||||
|
||||
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": WeatherSource.SYNTHETIC.value
|
||||
}
|
||||
|
||||
def generate_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=start_offset + i)
|
||||
forecast_day = self._generate_forecast_day(forecast_date, start_offset + i)
|
||||
forecast.append(forecast_day)
|
||||
|
||||
return forecast
|
||||
|
||||
async def generate_forecast(self, days: int) -> List[Dict[str, Any]]:
|
||||
"""Generate synthetic forecast data (async version for compatibility)"""
|
||||
return self.generate_forecast_sync(days, 0)
|
||||
|
||||
def generate_historical_data(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:
|
||||
historical_day = self._generate_historical_day(current_date)
|
||||
historical_data.append(historical_day)
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return historical_data
|
||||
|
||||
def _calculate_current_temperature(self, month: int, hour: int) -> float:
|
||||
"""Calculate current temperature based on seasonal and daily patterns"""
|
||||
base_temp = AEMETConstants.BASE_TEMPERATURE_SEASONAL + (month - 1) * AEMETConstants.TEMPERATURE_SEASONAL_MULTIPLIER
|
||||
temp_variation = math.sin((hour - 6) * math.pi / 12) * AEMETConstants.DAILY_TEMPERATURE_AMPLITUDE
|
||||
return base_temp + temp_variation
|
||||
|
||||
def _calculate_current_precipitation(self, now: datetime, month: int) -> float:
|
||||
"""Calculate current precipitation based on seasonal patterns"""
|
||||
rain_prob = 0.3 if month in [11, 12, 1, 2, 3] else 0.1
|
||||
return 2.5 if hash(now.date()) % 100 < rain_prob * 100 else 0.0
|
||||
|
||||
def _generate_forecast_day(self, forecast_date: datetime.date, day_offset: int) -> Dict[str, Any]:
|
||||
"""Generate a single forecast day"""
|
||||
month = forecast_date.month
|
||||
base_temp = AEMETConstants.BASE_TEMPERATURE_SEASONAL + (month - 1) * AEMETConstants.TEMPERATURE_SEASONAL_MULTIPLIER
|
||||
temp_variation = ((day_offset) % 7 - 3) * 2 # Weekly variation
|
||||
|
||||
return {
|
||||
"forecast_date": datetime.combine(forecast_date, datetime.min.time()),
|
||||
"generated_at": datetime.now(),
|
||||
"temperature": round(base_temp + temp_variation, 1),
|
||||
"precipitation": 2.0 if day_offset % 5 == 0 else 0.0,
|
||||
"humidity": 50 + (day_offset % 30),
|
||||
"wind_speed": 10 + (day_offset % 15),
|
||||
"description": "Lluvioso" if day_offset % 5 == 0 else "Soleado",
|
||||
"source": WeatherSource.SYNTHETIC.value
|
||||
}
|
||||
|
||||
def _generate_historical_day(self, date: datetime) -> Dict[str, Any]:
|
||||
"""Generate a single historical day"""
|
||||
month = date.month
|
||||
base_temp = AEMETConstants.BASE_TEMPERATURE_SEASONAL + (month - 1) * AEMETConstants.TEMPERATURE_SEASONAL_MULTIPLIER
|
||||
temp_variation = math.sin(date.day * 0.3) * 5
|
||||
|
||||
return {
|
||||
"date": date,
|
||||
"temperature": round(base_temp + temp_variation, 1),
|
||||
"precipitation": 1.5 if date.day % 7 == 0 else 0.0,
|
||||
"humidity": 45 + (date.day % 40),
|
||||
"wind_speed": 8 + (date.day % 20),
|
||||
"pressure": 1013 + math.sin(date.day * 0.2) * 20,
|
||||
"description": "Variable",
|
||||
"source": WeatherSource.SYNTHETIC.value
|
||||
}
|
||||
|
||||
|
||||
class LocationService:
|
||||
@@ -520,7 +547,6 @@ class AEMETClient(BaseAPIClient):
|
||||
self.timeout = httpx.Timeout(float(settings.AEMET_TIMEOUT))
|
||||
self.retries = settings.AEMET_RETRY_ATTEMPTS
|
||||
self.parser = WeatherDataParser()
|
||||
self.synthetic_generator = SyntheticWeatherGenerator()
|
||||
self.location_service = LocationService()
|
||||
|
||||
async def get_current_weather(self, latitude: float, longitude: float) -> Optional[Dict[str, Any]]:
|
||||
@@ -531,15 +557,15 @@ class AEMETClient(BaseAPIClient):
|
||||
|
||||
# Check if API key is configured
|
||||
if not self.api_key:
|
||||
logger.error("❌ AEMET API key not configured - falling back to synthetic data")
|
||||
return await self._get_synthetic_current_weather()
|
||||
logger.error("❌ AEMET API key not configured")
|
||||
return None
|
||||
|
||||
station_id = self.location_service.find_nearest_station(latitude, longitude)
|
||||
if not station_id:
|
||||
logger.warning("❌ No weather station found for coordinates",
|
||||
lat=latitude, lon=longitude,
|
||||
madrid_bounds=f"{AEMETConstants.MADRID_BOUNDS.min_lat}-{AEMETConstants.MADRID_BOUNDS.max_lat}")
|
||||
return await self._get_synthetic_current_weather()
|
||||
return None
|
||||
|
||||
logger.info("✅ Found nearest weather station", station_id=station_id)
|
||||
|
||||
@@ -555,22 +581,22 @@ class AEMETClient(BaseAPIClient):
|
||||
description=parsed_data.get("description"))
|
||||
return parsed_data
|
||||
|
||||
logger.warning("⚠️ AEMET API connectivity issues - using synthetic data",
|
||||
logger.warning("⚠️ AEMET API connectivity issues",
|
||||
station_id=station_id, reason="aemet_api_unreachable")
|
||||
return await self._get_synthetic_current_weather()
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error("❌ AEMET API failed - falling back to synthetic",
|
||||
logger.error("❌ AEMET API failed",
|
||||
error=str(e), error_type=type(e).__name__)
|
||||
return await self._get_synthetic_current_weather()
|
||||
return None
|
||||
|
||||
async def get_forecast(self, latitude: float, longitude: float, days: int = 7) -> List[Dict[str, Any]]:
|
||||
"""Get weather forecast for coordinates"""
|
||||
try:
|
||||
municipality_code = self.location_service.get_municipality_code(latitude, longitude)
|
||||
if not municipality_code:
|
||||
logger.info("No municipality code found, using synthetic data")
|
||||
return await self.synthetic_generator.generate_forecast(days)
|
||||
logger.error("No municipality code found for coordinates", lat=latitude, lon=longitude)
|
||||
return []
|
||||
|
||||
forecast_data = await self._fetch_forecast_data(municipality_code)
|
||||
if forecast_data:
|
||||
@@ -578,12 +604,47 @@ class AEMETClient(BaseAPIClient):
|
||||
if parsed_forecast:
|
||||
return parsed_forecast
|
||||
|
||||
logger.info("Falling back to synthetic forecast data", reason="invalid_forecast_data")
|
||||
return await self.synthetic_generator.generate_forecast(days)
|
||||
logger.error("Invalid forecast data received from AEMET API")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get weather forecast", error=str(e))
|
||||
return await self.synthetic_generator.generate_forecast(days)
|
||||
return []
|
||||
|
||||
async def get_hourly_forecast(self, latitude: float, longitude: float, hours: int = 48) -> List[Dict[str, Any]]:
|
||||
"""Get hourly weather forecast using AEMET's hourly prediction API"""
|
||||
try:
|
||||
logger.info("🕒 Getting hourly forecast from AEMET",
|
||||
lat=latitude, lon=longitude, hours=hours)
|
||||
|
||||
# Check if API key is configured
|
||||
if not self.api_key:
|
||||
logger.error("❌ AEMET API key not configured")
|
||||
return []
|
||||
|
||||
municipality_code = self.location_service.get_municipality_code(latitude, longitude)
|
||||
if not municipality_code:
|
||||
logger.error("❌ No municipality code found for coordinates",
|
||||
lat=latitude, lon=longitude)
|
||||
return []
|
||||
|
||||
logger.info("✅ Found municipality code", municipality_code=municipality_code)
|
||||
|
||||
hourly_data = await self._fetch_hourly_forecast_data(municipality_code)
|
||||
if hourly_data:
|
||||
logger.info("🎉 SUCCESS: Real AEMET hourly data retrieved!", municipality_code=municipality_code)
|
||||
parsed_data = self.parser.parse_hourly_forecast_data(hourly_data, hours)
|
||||
if parsed_data:
|
||||
return parsed_data
|
||||
|
||||
logger.error("⚠️ AEMET hourly API connectivity issues",
|
||||
municipality_code=municipality_code, reason="aemet_hourly_api_unreachable")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error("❌ AEMET hourly API failed",
|
||||
error=str(e), error_type=type(e).__name__)
|
||||
return []
|
||||
|
||||
async def get_historical_weather(self,
|
||||
latitude: float,
|
||||
@@ -598,9 +659,9 @@ class AEMETClient(BaseAPIClient):
|
||||
|
||||
station_id = self.location_service.find_nearest_station(latitude, longitude)
|
||||
if not station_id:
|
||||
logger.warning("No weather station found for historical data",
|
||||
logger.error("No weather station found for historical data",
|
||||
lat=latitude, lon=longitude)
|
||||
return self.synthetic_generator.generate_historical_data(start_date, end_date)
|
||||
return []
|
||||
|
||||
historical_data = await self._fetch_historical_data_in_chunks(
|
||||
station_id, start_date, end_date
|
||||
@@ -611,12 +672,12 @@ class AEMETClient(BaseAPIClient):
|
||||
total_count=len(historical_data))
|
||||
return historical_data
|
||||
else:
|
||||
logger.info("No real historical data available, using synthetic data")
|
||||
return self.synthetic_generator.generate_historical_data(start_date, end_date)
|
||||
logger.error("No real historical data available from AEMET")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get historical weather from AEMET API", error=str(e))
|
||||
return self.synthetic_generator.generate_historical_data(start_date, end_date)
|
||||
return []
|
||||
|
||||
async def _fetch_current_weather_data(self, station_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch current weather data from AEMET API"""
|
||||
@@ -646,6 +707,17 @@ class AEMETClient(BaseAPIClient):
|
||||
datos_url = initial_response.get("datos")
|
||||
return await self._fetch_from_url(datos_url)
|
||||
|
||||
async def _fetch_hourly_forecast_data(self, municipality_code: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Fetch hourly forecast data from AEMET API"""
|
||||
endpoint = f"/prediccion/especifica/municipio/horaria/{municipality_code}"
|
||||
initial_response = await self._get(endpoint)
|
||||
|
||||
if not self._is_valid_initial_response(initial_response):
|
||||
return None
|
||||
|
||||
datos_url = initial_response.get("datos")
|
||||
return await self._fetch_from_url(datos_url)
|
||||
|
||||
async def _fetch_historical_data_in_chunks(self,
|
||||
station_id: str,
|
||||
start_date: datetime,
|
||||
@@ -725,6 +797,3 @@ class AEMETClient(BaseAPIClient):
|
||||
return (response and isinstance(response, dict) and
|
||||
response.get("datos") and isinstance(response.get("datos"), str))
|
||||
|
||||
async def _get_synthetic_current_weather(self) -> Dict[str, Any]:
|
||||
"""Get synthetic current weather data"""
|
||||
return self.synthetic_generator.generate_current_weather()
|
||||
18
services/external/app/schemas/weather.py
vendored
18
services/external/app/schemas/weather.py
vendored
@@ -158,4 +158,20 @@ class HistoricalWeatherRequest(BaseModel):
|
||||
class WeatherForecastRequest(BaseModel):
|
||||
latitude: float
|
||||
longitude: float
|
||||
days: int
|
||||
days: int
|
||||
|
||||
class HourlyForecastRequest(BaseModel):
|
||||
latitude: float
|
||||
longitude: float
|
||||
hours: int = Field(default=48, ge=1, le=48, description="Number of hours to forecast (1-48)")
|
||||
|
||||
class HourlyForecastResponse(BaseModel):
|
||||
forecast_datetime: datetime
|
||||
generated_at: datetime
|
||||
temperature: Optional[float]
|
||||
precipitation: Optional[float]
|
||||
humidity: Optional[float]
|
||||
wind_speed: Optional[float]
|
||||
description: Optional[str]
|
||||
source: str
|
||||
hour: int
|
||||
@@ -9,7 +9,7 @@ import structlog
|
||||
|
||||
from app.models.weather import WeatherData, WeatherForecast
|
||||
from app.external.aemet import AEMETClient
|
||||
from app.schemas.weather import WeatherDataResponse, WeatherForecastResponse
|
||||
from app.schemas.weather import WeatherDataResponse, WeatherForecastResponse, HourlyForecastResponse
|
||||
from app.repositories.weather_repository import WeatherRepository
|
||||
|
||||
logger = structlog.get_logger()
|
||||
@@ -79,6 +79,48 @@ class WeatherService:
|
||||
logger.error("Failed to get weather forecast", error=str(e), lat=latitude, lon=longitude)
|
||||
return []
|
||||
|
||||
async def get_hourly_forecast(self, latitude: float, longitude: float, hours: int = 48) -> List[HourlyForecastResponse]:
|
||||
"""Get hourly weather forecast for location"""
|
||||
try:
|
||||
logger.debug("Getting hourly weather forecast", lat=latitude, lon=longitude, hours=hours)
|
||||
hourly_data = await self.aemet_client.get_hourly_forecast(latitude, longitude, hours)
|
||||
|
||||
if hourly_data:
|
||||
logger.debug("Hourly forecast data received", count=len(hourly_data))
|
||||
# Validate each hourly forecast item before creating response
|
||||
valid_forecasts = []
|
||||
for item in hourly_data:
|
||||
try:
|
||||
if isinstance(item, dict):
|
||||
# Ensure required fields are present
|
||||
hourly_item = {
|
||||
"forecast_datetime": item.get("forecast_datetime", datetime.now()),
|
||||
"generated_at": item.get("generated_at", datetime.now()),
|
||||
"temperature": float(item.get("temperature", 15.0)),
|
||||
"precipitation": float(item.get("precipitation", 0.0)),
|
||||
"humidity": float(item.get("humidity", 50.0)),
|
||||
"wind_speed": float(item.get("wind_speed", 10.0)),
|
||||
"description": str(item.get("description", "Variable")),
|
||||
"source": str(item.get("source", "unknown")),
|
||||
"hour": int(item.get("hour", 0))
|
||||
}
|
||||
valid_forecasts.append(HourlyForecastResponse(**hourly_item))
|
||||
else:
|
||||
logger.warning("Invalid hourly forecast item type", item_type=type(item))
|
||||
except Exception as item_error:
|
||||
logger.warning("Error processing hourly forecast item", error=str(item_error), item=item)
|
||||
continue
|
||||
|
||||
logger.debug("Valid hourly forecasts processed", count=len(valid_forecasts))
|
||||
return valid_forecasts
|
||||
else:
|
||||
logger.warning("No hourly forecast data received from AEMET client")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get hourly weather forecast", error=str(e), lat=latitude, lon=longitude)
|
||||
return []
|
||||
|
||||
async def get_historical_weather(self,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
|
||||
Reference in New Issue
Block a user