Files
Ai/weather/services.py
T

185 lines
6.0 KiB
Python
Raw Normal View History

2026-03-19 22:54:29 +03:30
"""
سرویس‌های هواشناسی — واکشی پیش‌بینی ۷ روزه و ذخیره در دیتابیس.
"""
import logging
from datetime import date, timedelta
2026-04-29 01:27:29 +03:30
from django.apps import apps
2026-03-19 22:54:29 +03:30
from django.db import transaction
from location_data.models import SoilLocation
2026-04-29 01:27:29 +03:30
from .adapters import DEFAULT_FORECAST_DAYS
2026-03-19 22:54:29 +03:30
from .models import WeatherForecast
logger = logging.getLogger(__name__)
def fetch_weather_from_api(latitude: float, longitude: float) -> dict | None:
"""
2026-04-29 01:27:29 +03:30
واکشی پیش‌بینی هواشناسی از provider فعال.
خروجی در قالب سازگار با Open-Meteo daily format برگردانده می‌شود.
2026-03-19 22:54:29 +03:30
"""
2026-04-29 01:27:29 +03:30
adapter = apps.get_app_config("weather").get_weather_data_adapter()
return adapter.fetch_forecast(
latitude=latitude,
longitude=longitude,
days=DEFAULT_FORECAST_DAYS,
2026-04-07 01:08:41 +03:30
)
2026-03-19 22:54:29 +03:30
def parse_weather_response(data: dict) -> list[dict]:
"""
تبدیل پاسخ API به لیست dict برای ذخیره در WeatherForecast.
فرمت ورودی: Open-Meteo daily format.
"""
daily = data.get("daily", {})
times = daily.get("time", [])
forecasts = []
2026-04-29 01:27:29 +03:30
for index, date_str in enumerate(times):
2026-03-19 22:54:29 +03:30
forecasts.append(
{
"forecast_date": date_str,
2026-04-29 01:27:29 +03:30
"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),
2026-03-19 22:54:29 +03:30
"precipitation_probability": _safe_index(
2026-04-29 01:27:29 +03:30
daily.get("precipitation_probability_max"),
index,
2026-03-19 22:54:29 +03:30
),
"humidity_mean": _safe_index(
2026-04-29 01:27:29 +03:30
daily.get("relative_humidity_2m_mean"),
index,
2026-03-19 22:54:29 +03:30
),
2026-04-29 01:27:29 +03:30
"wind_speed_max": _safe_index(daily.get("wind_speed_10m_max"), index),
2026-03-19 22:54:29 +03:30
"et0": _safe_index(
2026-04-29 01:27:29 +03:30
daily.get("et0_fao_evapotranspiration"),
index,
2026-03-19 22:54:29 +03:30
),
2026-04-29 01:27:29 +03:30
"weather_code": _safe_index(daily.get("weather_code"), index),
2026-03-19 22:54:29 +03:30
}
)
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:
2026-04-29 01:27:29 +03:30
logger.info("Weather provider returned no data for location %s.", location.id)
2026-03-19 22:54:29 +03:30
return {
"status": "no_data",
"location_id": location.id,
2026-04-29 01:27:29 +03:30
"message": "Weather provider returned no data.",
2026-03-19 22:54:29 +03:30
}
forecasts = parse_weather_response(data)
with transaction.atomic():
2026-04-29 01:27:29 +03:30
for forecast in forecasts:
2026-03-19 22:54:29 +03:30
WeatherForecast.objects.update_or_create(
location=location,
2026-04-29 01:27:29 +03:30
forecast_date=forecast.pop("forecast_date"),
defaults=forecast,
2026-03-19 22:54:29 +03:30
)
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():
2026-04-29 01:27:29 +03:30
results.append(update_weather_for_location(location))
2026-03-19 22:54:29 +03:30
return results
def get_forecast_for_location(
location: SoilLocation,
2026-04-29 01:27:29 +03:30
days: int = DEFAULT_FORECAST_DAYS,
2026-03-19 22:54:29 +03:30
) -> 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": "بارش فردا ناچیز یا صفر — آبیاری توصیه می‌شود.",
}