UPDATE
This commit is contained in:
+55
-68
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user