2026-04-25 17:22:41 +03:30
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from farm_data.models import SensorData
|
2026-05-13 16:45:54 +03:30
|
|
|
from farm_data.services import build_ai_farm_snapshot
|
2026-04-25 17:22:41 +03:30
|
|
|
|
|
|
|
|
from .services import get_forecast_for_location
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
WMO_CONDITIONS = {
|
|
|
|
|
0: "صاف",
|
|
|
|
|
1: "عمدتاً صاف",
|
|
|
|
|
2: "نیمهابری",
|
|
|
|
|
3: "ابری",
|
|
|
|
|
45: "مه",
|
|
|
|
|
48: "مه یخزده",
|
|
|
|
|
51: "نمنم باران",
|
|
|
|
|
61: "بارش خفیف",
|
|
|
|
|
63: "بارش متوسط",
|
|
|
|
|
65: "بارش شدید",
|
|
|
|
|
71: "برف خفیف",
|
|
|
|
|
80: "رگبار",
|
|
|
|
|
95: "رعد و برق",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _safe_number(value, default=0):
|
|
|
|
|
return default if value is None else value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _average(values, default=0):
|
|
|
|
|
clean_values = [value for value in values if value is not None]
|
|
|
|
|
if not clean_values:
|
|
|
|
|
return default
|
|
|
|
|
return sum(clean_values) / len(clean_values)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _weather_condition(weather_code):
|
|
|
|
|
return WMO_CONDITIONS.get(weather_code, "نامشخص")
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 16:45:54 +03:30
|
|
|
def _build_farm_weather_card(forecasts: list[Any], *, farm_uuid: str | None = None, ai_snapshot: dict[str, Any] | None = None) -> dict[str, Any]:
|
2026-04-25 17:22:41 +03:30
|
|
|
if not forecasts:
|
|
|
|
|
return {
|
|
|
|
|
"condition": "نامشخص",
|
|
|
|
|
"temperature": 0,
|
|
|
|
|
"unit": "°C",
|
|
|
|
|
"humidity": 0,
|
|
|
|
|
"windSpeed": 0,
|
|
|
|
|
"windUnit": "km/h",
|
|
|
|
|
"chartData": {"labels": [], "series": [[]]},
|
2026-05-13 16:45:54 +03:30
|
|
|
"source_metadata": {
|
|
|
|
|
"weather": {"source": "center_location_forecast", "policy": "center_location_latest_forecast"},
|
|
|
|
|
"agronomic_metrics": {"source": "build_ai_farm_snapshot", "policy": "cluster_block_farm_aggregated"},
|
|
|
|
|
},
|
2026-04-25 17:22:41 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current_forecast = forecasts[0]
|
|
|
|
|
labels = [str(forecast.forecast_date) for forecast in forecasts[:7]]
|
|
|
|
|
series = [[round(_safe_number(forecast.temperature_mean, 0)) for forecast in forecasts[:7]]]
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"condition": _weather_condition(current_forecast.weather_code),
|
|
|
|
|
"temperature": round(_safe_number(current_forecast.temperature_mean, current_forecast.temperature_max)),
|
|
|
|
|
"unit": "°C",
|
|
|
|
|
"humidity": round(_average([current_forecast.humidity_mean], default=0)),
|
|
|
|
|
"windSpeed": round(_safe_number(current_forecast.wind_speed_max, 0)),
|
|
|
|
|
"windUnit": "km/h",
|
|
|
|
|
"chartData": {
|
|
|
|
|
"labels": labels,
|
|
|
|
|
"series": series,
|
|
|
|
|
},
|
2026-05-13 16:45:54 +03:30
|
|
|
"source_metadata": {
|
|
|
|
|
"weather": {"source": "center_location_forecast", "policy": "center_location_latest_forecast"},
|
|
|
|
|
"agronomic_metrics": {
|
|
|
|
|
"source": "build_ai_farm_snapshot",
|
|
|
|
|
"policy": "cluster_block_farm_aggregated",
|
|
|
|
|
"available": bool(ai_snapshot),
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-04-25 17:22:41 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FarmWeatherService:
|
|
|
|
|
def get_farm_weather_card(self, *, farm_uuid: str) -> dict[str, Any]:
|
|
|
|
|
sensor = (
|
|
|
|
|
SensorData.objects.select_related("center_location")
|
|
|
|
|
.filter(farm_uuid=farm_uuid)
|
|
|
|
|
.first()
|
|
|
|
|
)
|
|
|
|
|
if sensor is None:
|
|
|
|
|
raise ValueError("Farm not found.")
|
|
|
|
|
|
|
|
|
|
forecasts = get_forecast_for_location(sensor.center_location, days=7)
|
2026-05-13 16:45:54 +03:30
|
|
|
ai_snapshot = build_ai_farm_snapshot(farm_uuid)
|
|
|
|
|
return _build_farm_weather_card(forecasts, farm_uuid=farm_uuid, ai_snapshot=ai_snapshot)
|