185 lines
6.0 KiB
Python
185 lines
6.0 KiB
Python
"""
|
|
سرویسهای هواشناسی — واکشی پیشبینی ۷ روزه و ذخیره در دیتابیس.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import date, timedelta
|
|
|
|
from django.apps import apps
|
|
from django.db import transaction
|
|
|
|
from location_data.models import SoilLocation
|
|
|
|
from .adapters import DEFAULT_FORECAST_DAYS
|
|
from .models import WeatherForecast
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def fetch_weather_from_api(latitude: float, longitude: float) -> dict | None:
|
|
"""
|
|
واکشی پیشبینی هواشناسی از provider فعال.
|
|
خروجی در قالب سازگار با Open-Meteo daily format برگردانده میشود.
|
|
"""
|
|
adapter = apps.get_app_config("weather").get_weather_data_adapter()
|
|
return adapter.fetch_forecast(
|
|
latitude=latitude,
|
|
longitude=longitude,
|
|
days=DEFAULT_FORECAST_DAYS,
|
|
)
|
|
|
|
|
|
def parse_weather_response(data: dict) -> list[dict]:
|
|
"""
|
|
تبدیل پاسخ API به لیست dict برای ذخیره در WeatherForecast.
|
|
فرمت ورودی: Open-Meteo daily format.
|
|
"""
|
|
daily = data.get("daily", {})
|
|
times = daily.get("time", [])
|
|
forecasts = []
|
|
|
|
for index, date_str in enumerate(times):
|
|
forecasts.append(
|
|
{
|
|
"forecast_date": date_str,
|
|
"temperature_max": _safe_index(daily.get("temperature_2m_max"), index),
|
|
"temperature_min": _safe_index(daily.get("temperature_2m_min"), index),
|
|
"temperature_mean": _safe_index(daily.get("temperature_2m_mean"), index),
|
|
"precipitation": _safe_index(daily.get("precipitation_sum"), index),
|
|
"precipitation_probability": _safe_index(
|
|
daily.get("precipitation_probability_max"),
|
|
index,
|
|
),
|
|
"humidity_mean": _safe_index(
|
|
daily.get("relative_humidity_2m_mean"),
|
|
index,
|
|
),
|
|
"wind_speed_max": _safe_index(daily.get("wind_speed_10m_max"), index),
|
|
"et0": _safe_index(
|
|
daily.get("et0_fao_evapotranspiration"),
|
|
index,
|
|
),
|
|
"weather_code": _safe_index(daily.get("weather_code"), index),
|
|
}
|
|
)
|
|
return forecasts
|
|
|
|
|
|
def _safe_index(lst: list | None, index: int):
|
|
"""مقدار index را از لیست برمیگرداند یا None."""
|
|
if lst is None or index >= len(lst):
|
|
return None
|
|
return lst[index]
|
|
|
|
|
|
def update_weather_for_location(location: SoilLocation) -> dict:
|
|
"""
|
|
واکشی و ذخیره پیشبینی هواشناسی ۷ روزه برای یک SoilLocation.
|
|
|
|
خروجی:
|
|
{"status": "success"|"no_data"|"error", "location_id": int, ...}
|
|
"""
|
|
lat = float(location.latitude)
|
|
lon = float(location.longitude)
|
|
|
|
try:
|
|
data = fetch_weather_from_api(lat, lon)
|
|
except Exception as exc:
|
|
logger.error("Weather API error for location %s: %s", location.id, exc)
|
|
return {
|
|
"status": "error",
|
|
"location_id": location.id,
|
|
"error": str(exc),
|
|
}
|
|
|
|
if data is None:
|
|
logger.info("Weather provider returned no data for location %s.", location.id)
|
|
return {
|
|
"status": "no_data",
|
|
"location_id": location.id,
|
|
"message": "Weather provider returned no data.",
|
|
}
|
|
|
|
forecasts = parse_weather_response(data)
|
|
|
|
with transaction.atomic():
|
|
for forecast in forecasts:
|
|
WeatherForecast.objects.update_or_create(
|
|
location=location,
|
|
forecast_date=forecast.pop("forecast_date"),
|
|
defaults=forecast,
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"location_id": location.id,
|
|
"days_updated": len(forecasts),
|
|
}
|
|
|
|
|
|
def update_weather_for_all_locations() -> list[dict]:
|
|
"""
|
|
واکشی پیشبینی هواشناسی برای تمام SoilLocationهای موجود.
|
|
"""
|
|
results = []
|
|
for location in SoilLocation.objects.all():
|
|
results.append(update_weather_for_location(location))
|
|
return results
|
|
|
|
|
|
def get_forecast_for_location(
|
|
location: SoilLocation,
|
|
days: int = DEFAULT_FORECAST_DAYS,
|
|
) -> list[WeatherForecast]:
|
|
"""
|
|
دریافت پیشبینیهای ذخیرهشده برای یک location (تا N روز آینده).
|
|
"""
|
|
today = date.today()
|
|
end_date = today + timedelta(days=days)
|
|
return list(
|
|
WeatherForecast.objects.filter(
|
|
location=location,
|
|
forecast_date__gte=today,
|
|
forecast_date__lte=end_date,
|
|
).order_by("forecast_date")
|
|
)
|
|
|
|
|
|
def should_irrigate_today(location: SoilLocation) -> dict:
|
|
"""
|
|
بررسی ساده: آیا فردا باران میبارد؟
|
|
اگر بارش فردا بیشتر از آستانه باشد → آبیاری لازم نیست.
|
|
"""
|
|
tomorrow = date.today() + timedelta(days=1)
|
|
forecast = WeatherForecast.objects.filter(
|
|
location=location,
|
|
forecast_date=tomorrow,
|
|
).first()
|
|
|
|
if forecast is None:
|
|
return {
|
|
"needs_irrigation": None,
|
|
"tomorrow_precipitation": None,
|
|
"tomorrow_date": str(tomorrow),
|
|
"reason": "داده پیشبینی فردا موجود نیست.",
|
|
}
|
|
|
|
rain_threshold_mm = 2.0
|
|
if forecast.precipitation is not None and forecast.precipitation >= rain_threshold_mm:
|
|
return {
|
|
"needs_irrigation": False,
|
|
"tomorrow_precipitation": forecast.precipitation,
|
|
"tomorrow_date": str(tomorrow),
|
|
"reason": (
|
|
f"فردا {forecast.precipitation} mm بارش پیشبینی شده — "
|
|
"نیاز به آبیاری نیست."
|
|
),
|
|
}
|
|
|
|
return {
|
|
"needs_irrigation": True,
|
|
"tomorrow_precipitation": forecast.precipitation,
|
|
"tomorrow_date": str(tomorrow),
|
|
"reason": "بارش فردا ناچیز یا صفر — آبیاری توصیه میشود.",
|
|
}
|