""" سرویس‌های هواشناسی — واکشی پیش‌بینی ۷ روزه و ذخیره در دیتابیس. """ import logging from datetime import date, timedelta from django.conf import settings from django.db import transaction from location_data.models import SoilLocation from .models import WeatherForecast logger = logging.getLogger(__name__) def fetch_weather_from_api(latitude: float, longitude: float) -> dict | None: """ اتصال به API هواشناسی و دریافت پیش‌بینی ۷ روزه. TODO: پیاده‌سازی اتصال واقعی به API (مثلاً Open-Meteo). در حال حاضر این تابع خالی است و None برمی‌گرداند. پارامترها: latitude: عرض جغرافیایی longitude: طول جغرافیایی خروجی مورد انتظار (وقتی پیاده‌سازی شود): { "daily": { "time": ["2025-07-01", "2025-07-02", ...], "temperature_2m_max": [35.2, 36.1, ...], "temperature_2m_min": [22.1, 23.0, ...], "temperature_2m_mean": [28.6, 29.5, ...], "precipitation_sum": [0.0, 2.5, ...], "precipitation_probability_max": [0, 60, ...], "relative_humidity_2m_mean": [30.0, 45.0, ...], "wind_speed_10m_max": [15.0, 20.0, ...], "et0_fao_evapotranspiration": [6.5, 5.8, ...], "weather_code": [0, 61, ...], } } """ # TODO: اتصال واقعی به API هواشناسی # api_url = settings.WEATHER_API_BASE_URL # api_key = settings.WEATHER_API_KEY return None 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 i, date_str in enumerate(times): forecasts.append( { "forecast_date": date_str, "temperature_max": _safe_index( daily.get("temperature_2m_max"), i ), "temperature_min": _safe_index( daily.get("temperature_2m_min"), i ), "temperature_mean": _safe_index( daily.get("temperature_2m_mean"), i ), "precipitation": _safe_index( daily.get("precipitation_sum"), i ), "precipitation_probability": _safe_index( daily.get("precipitation_probability_max"), i ), "humidity_mean": _safe_index( daily.get("relative_humidity_2m_mean"), i ), "wind_speed_max": _safe_index( daily.get("wind_speed_10m_max"), i ), "et0": _safe_index( daily.get("et0_fao_evapotranspiration"), i ), "weather_code": _safe_index( daily.get("weather_code"), i ), } ) 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 API returned no data for location %s (stub mode).", location.id, ) return { "status": "no_data", "location_id": location.id, "message": "API connection not implemented yet.", } forecasts = parse_weather_response(data) with transaction.atomic(): for fc in forecasts: WeatherForecast.objects.update_or_create( location=location, forecast_date=fc.pop("forecast_date"), defaults=fc, ) 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(): result = update_weather_for_location(location) results.append(result) return results def get_forecast_for_location( location: SoilLocation, days: int = 7, ) -> 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: """ بررسی ساده: آیا فردا باران می‌بارد؟ اگر بارش فردا بیشتر از آستانه باشد → آبیاری لازم نیست. خروجی: { "needs_irrigation": bool | None, "tomorrow_precipitation": float | None, "tomorrow_date": str, "reason": str, } """ 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": "بارش فردا ناچیز یا صفر — آبیاری توصیه می‌شود.", }