diff --git a/src/api/api_v1/endpoints/forecast.py b/src/api/api_v1/endpoints/forecast.py index 80db6db..69bebe8 100644 --- a/src/api/api_v1/endpoints/forecast.py +++ b/src/api/api_v1/endpoints/forecast.py @@ -22,7 +22,7 @@ from src.external_services.openmeteo import WeatherClientFactory from src.schemas.point import GeoJSONOut -from src.schemas.history_data import HourlyObservationOut, HourlyResponse +from src.schemas.history_data import DailyResponse, HourlyObservationOut, HourlyResponse from src.schemas.spray import SprayForecastResponse from src.models.spray import SprayStatus from src import utils @@ -165,3 +165,50 @@ async def get_hourly_spray_forecast( ) return results + + +# --------------------------------------------------------------------------- +# Daily irrigation forecast +# --------------------------------------------------------------------------- + +@router.get( + "/daily/irrigation/", + response_model=DailyResponse, + summary="Daily weather forecast for smart irrigation (7–16 days)", +) +async def get_daily_irrigation_forecast( + lat: float = Query(..., description="Latitude", example=38.25), + lon: float = Query(..., description="Longitude", example=21.74), + days: int = Query( + 16, ge=1, le=16, + description="Number of forecast days (including today)", + ), +): + """ + Returns **daily** weather data optimized for smart irrigation systems. + + Variables returned per day: + - **precipitation_sum** – total precipitation (mm) + - **precipitation_probability_max** – maximum precipitation probability (%) + - **temperature_2m_min** – minimum temperature (°C) + - **temperature_2m_max** – maximum temperature (°C) + - **et0_fao_evapotranspiration** – reference evapotranspiration ET₀ (mm) + + Data comes from the Open-Meteo Forecast API. + Supports up to 16 days of forecast. + """ + client = WeatherClientFactory.get_provider() + try: + results = await client.get_daily_forecast(lat, lon, days=days) + except Exception as e: + logger.error(f"Error fetching daily forecast from Open-Meteo: {e}") + raise HTTPException( + status_code=502, + detail="Could not retrieve daily forecast from Open-Meteo", + ) from e + + return DailyResponse( + location={"lat": lat, "lon": lon}, + data=results, + source="open-meteo", + ) diff --git a/src/external_services/openmeteo.py b/src/external_services/openmeteo.py index 5ec26b3..d33d7a5 100644 --- a/src/external_services/openmeteo.py +++ b/src/external_services/openmeteo.py @@ -27,6 +27,11 @@ async def get_hourly_forecast( ) -> List[HourlyObservationOut]: ... + async def get_daily_forecast( + self, lat: float, lon: float, days: int = 16 + ) -> List[DailyObservationOut]: + ... + class OpenMeteoClient: BASE_URL = "https://archive-api.open-meteo.com/v1/archive" @@ -165,6 +170,73 @@ async def get_hourly_forecast( return results +# ---- Daily forecast (Open-Meteo Forecast API) ---- + # Variables for smart irrigation: precipitation, probability, temp range, ET0 + DAILY_FORECAST_VARIABLES = [ + "precipitation_sum", + "precipitation_probability_max", + "temperature_2m_min", + "temperature_2m_max", + "et0_fao_evapotranspiration", + ] + + async def get_daily_forecast( + self, lat: float, lon: float, days: int = 16 + ) -> List[DailyObservationOut]: + """ + Fetch daily weather forecast for irrigation planning from the + Open-Meteo **Forecast** API. + + Returns one :class:`DailyObservationOut` per day with: + - precipitation_sum (mm) + - precipitation_probability_max (%) + - temperature_2m_min (°C) + - temperature_2m_max (°C) + - et0_fao_evapotranspiration (mm) + + Parameters + ---------- + lat, lon : float + Location coordinates. + days : int + Number of forecast days (1–16, default 16). + """ + params = { + "latitude": lat, + "longitude": lon, + "daily": ",".join(self.DAILY_FORECAST_VARIABLES), + "timezone": "auto", + "forecast_days": days, + } + + data = await self._fetch_data(params, url=self.FORECAST_URL) + + if "daily" not in data: + logger.warning("Open-Meteo returned no daily forecast data") + return [] + + timestamps = data["daily"]["time"] + results: List[DailyObservationOut] = [] + + for i, t in enumerate(timestamps): + values: Dict[str, Union[float, None]] = {} + for var in self.DAILY_FORECAST_VARIABLES: + if var in data["daily"]: + values[var] = data["daily"][var][i] + results.append( + DailyObservationOut( + date=date.fromisoformat(t), + values=values, + ) + ) + + logger.info( + "Open-Meteo daily forecast: %d days for (%s, %s)", + len(results), lat, lon, + ) + return results + + # Factory using environment variable class WeatherClientFactory: _provider: Optional[WeatherProvider] = None