This commit is contained in:
2026-05-09 16:55:06 +03:30
parent 1679825ae2
commit cead7dafe2
51 changed files with 7514 additions and 1221 deletions
+55 -68
View File
@@ -12,11 +12,13 @@ from django.utils.dateparse import parse_datetime
import requests
from location_data.models import SoilLocation
from location_data.serializers import SoilDepthDataSerializer
from location_data.tasks import fetch_soil_data_for_coordinates
from location_data.block_subdivision import create_or_get_block_subdivision
from location_data.models import BlockSubdivision, SoilLocation
from location_data.satellite_snapshot import (
build_location_block_satellite_snapshots,
build_location_satellite_snapshot,
)
from irrigation.serializers import IrrigationMethodSerializer
from weather.services import update_weather_for_location
from weather.models import WeatherForecast
from .models import (
@@ -29,7 +31,6 @@ from .models import (
from .serializers import PlantCatalogSnapshotSerializer, WeatherForecastDetailSerializer
DEPTH_PRIORITY = ["0-5cm", "5-15cm", "15-30cm"]
DECIMAL_PRECISION = Decimal("0.000001")
logger = logging.getLogger(__name__)
@@ -231,7 +232,7 @@ def get_canonical_farm_record(farm_uuid: str) -> SensorData | None:
"weather_forecast",
"irrigation_method",
)
.prefetch_related("plant_assignments__plant", "center_location__depths")
.prefetch_related("plant_assignments__plant")
.filter(farm_uuid=farm_uuid)
.first()
)
@@ -461,14 +462,12 @@ def get_farm_details(farm_uuid: str):
center_location.weather_forecasts.order_by("-forecast_date", "-id").first()
)
depths = list(center_location.depths.all())
depths.sort(key=lambda item: DEPTH_PRIORITY.index(item.depth_label) if item.depth_label in DEPTH_PRIORITY else 99)
soil_metrics = _surface_soil_metrics(depths)
latest_satellite = build_location_satellite_snapshot(center_location)
soil_metrics = dict(latest_satellite.get("resolved_metrics") or {})
sensor_metrics, sensor_metric_sources = _resolve_sensor_metrics(farm.sensor_payload)
resolved_metrics = dict(soil_metrics)
metric_sources = {key: "soil" for key in soil_metrics}
metric_sources = {key: "remote_sensing" for key in soil_metrics}
for key, value in sensor_metrics.items():
resolved_metrics[key] = value
metric_sources[key] = sensor_metric_sources[key]
@@ -482,6 +481,8 @@ def get_farm_details(farm_uuid: str):
"lat": center_location.latitude,
"lon": center_location.longitude,
"farm_boundary": center_location.farm_boundary,
"input_block_count": center_location.input_block_count,
"block_layout": center_location.block_layout,
},
"weather": WeatherForecastDetailSerializer(weather).data if weather else None,
"sensor_payload": farm.sensor_payload or {},
@@ -489,7 +490,7 @@ def get_farm_details(farm_uuid: str):
"soil": {
"resolved_metrics": resolved_metrics,
"metric_sources": metric_sources,
"depths": SoilDepthDataSerializer(depths, many=True).data,
"satellite_snapshots": build_location_block_satellite_snapshots(center_location),
},
"plant_ids": [plant.backend_plant_id for plant in plant_snapshots],
"plants": PlantCatalogSnapshotSerializer(plant_snapshots, many=True).data,
@@ -516,7 +517,10 @@ def get_farm_details(farm_uuid: str):
}
def resolve_center_location_from_boundary(farm_boundary: dict | list) -> SoilLocation:
def resolve_center_location_from_boundary(
farm_boundary: dict | list,
block_count: int = 1,
) -> SoilLocation:
"""
مرز مزرعه را می‌گیرد، مرکز را محاسبه می‌کند و رکورد SoilLocation را
ایجاد/به‌روزرسانی می‌کند.
@@ -530,13 +534,35 @@ def resolve_center_location_from_boundary(farm_boundary: dict | list) -> SoilLoc
raise ValueError("farm_boundary باید حداقل 3 گوشه معتبر داشته باشد.")
center_lat, center_lon = _compute_polygon_centroid(normalized_points)
serialized_boundary = _serialize_boundary(farm_boundary)
normalized_block_count = max(int(block_count or 1), 1)
with transaction.atomic():
location, _ = SoilLocation.objects.update_or_create(
location, created = SoilLocation.objects.get_or_create(
latitude=center_lat,
longitude=center_lon,
defaults={"farm_boundary": _serialize_boundary(farm_boundary)},
defaults={
"farm_boundary": serialized_boundary,
"input_block_count": normalized_block_count,
},
)
if created:
location.set_input_block_count(normalized_block_count)
location.farm_boundary = serialized_boundary
location.save(update_fields=["farm_boundary", "input_block_count", "block_layout", "updated_at"])
if normalized_block_count == 1:
_create_initial_block_subdivision(location, serialized_boundary)
else:
changed_fields = []
if location.farm_boundary != serialized_boundary:
location.farm_boundary = serialized_boundary
changed_fields.append("farm_boundary")
if location.input_block_count != normalized_block_count:
location.set_input_block_count(normalized_block_count)
changed_fields.extend(["input_block_count", "block_layout"])
if changed_fields:
changed_fields.append("updated_at")
location.save(update_fields=changed_fields)
return location
@@ -550,36 +576,25 @@ def resolve_weather_for_location(location: SoilLocation) -> WeatherForecast | No
def ensure_location_and_weather_data(location: SoilLocation) -> tuple[SoilLocation, WeatherForecast | None]:
"""
اگر داده خاک یا آب‌وهوا برای location موجود نباشد، از سرویس مربوطه
واکشی و در دیتابیس ذخیره می‌شود.
در فاز فعلی برای location_data و بلوک‌ها هیچ ریکوئست خارجی زده نمی‌شود
و فقط داده‌های محلی موجود برگردانده می‌شوند.
"""
if not location.is_complete:
try:
soil_result = fetch_soil_data_for_coordinates(
latitude=float(location.latitude),
longitude=float(location.longitude),
)
except Exception as exc:
raise ExternalDataSyncError(f"خطا در واکشی داده خاک: {exc}") from exc
if soil_result.get("status") != "completed":
raise ExternalDataSyncError(
soil_result.get("error") or "واکشی داده خاک کامل نشد."
)
location.refresh_from_db()
weather_forecast = resolve_weather_for_location(location)
if weather_forecast is None:
weather_result = update_weather_for_location(location)
if weather_result.get("status") not in {"success", "no_data"}:
raise ExternalDataSyncError(
weather_result.get("error") or "واکشی داده آب‌وهوا کامل نشد."
)
weather_forecast = resolve_weather_for_location(location)
return location, weather_forecast
def _create_initial_block_subdivision(
location: SoilLocation,
block_boundary: dict | list,
) -> BlockSubdivision:
subdivision, _created = create_or_get_block_subdivision(
location=location,
block_code="block-1",
boundary=block_boundary,
)
return subdivision
def _resolve_sensor_metrics(sensor_payload: dict | None) -> tuple[dict, dict]:
if not isinstance(sensor_payload, dict):
return {}, {}
@@ -659,34 +674,6 @@ def _normalize_numeric_result(value: float, source_values: list[object]) -> int
return float(Decimal(str(value)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP))
def _surface_soil_metrics(depths) -> dict:
if not depths:
return {}
primary_depth = depths[0]
fields = [
"bdod",
"cec",
"cfvo",
"clay",
"nitrogen",
"ocd",
"ocs",
"phh2o",
"sand",
"silt",
"soc",
"wv0010",
"wv0033",
"wv1500",
]
return {
field: getattr(primary_depth, field)
for field in fields
if getattr(primary_depth, field) is not None
}
def _extract_boundary_points(boundary: dict | list) -> list:
if isinstance(boundary, dict):
if boundary.get("type") == "Polygon":