UPDATE
This commit is contained in:
@@ -3,7 +3,7 @@ from datetime import date
|
||||
|
||||
def load_farm_context(sensor_id: str) -> dict | None:
|
||||
from irrigation.models import IrrigationMethod
|
||||
from location_data.models import SoilDepthData
|
||||
from location_data.satellite_snapshot import build_location_block_satellite_snapshots
|
||||
from farm_data.models import SensorData
|
||||
from farm_data.services import get_farm_plant_snapshots
|
||||
from weather.models import WeatherForecast
|
||||
@@ -16,7 +16,7 @@ def load_farm_context(sensor_id: str) -> dict | None:
|
||||
return None
|
||||
|
||||
location = sensor.center_location
|
||||
depths = list(SoilDepthData.objects.filter(soil_location=location).order_by("depth_label"))
|
||||
satellite_snapshots = build_location_block_satellite_snapshots(location)
|
||||
forecasts = list(
|
||||
WeatherForecast.objects.filter(location=location, forecast_date__gte=date.today()).order_by("forecast_date")[:7]
|
||||
)
|
||||
@@ -26,7 +26,7 @@ def load_farm_context(sensor_id: str) -> dict | None:
|
||||
return {
|
||||
"sensor": sensor,
|
||||
"location": location,
|
||||
"depths": depths,
|
||||
"satellite_snapshots": satellite_snapshots,
|
||||
"forecasts": forecasts,
|
||||
"history": [],
|
||||
"plants": plants,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from location_data.serializers import SoilDepthDataSerializer
|
||||
from irrigation.models import IrrigationMethod
|
||||
from irrigation.serializers import IrrigationMethodSerializer
|
||||
from weather.models import WeatherForecast
|
||||
@@ -19,6 +18,7 @@ class SensorDataUpdateSerializer(serializers.Serializer):
|
||||
|
||||
farm_uuid = serializers.UUIDField(required=True)
|
||||
farm_boundary = serializers.JSONField(required=True)
|
||||
block_count = serializers.IntegerField(required=False, min_value=1, default=1)
|
||||
sensor_key = serializers.CharField(required=False, default=DEFAULT_SENSOR_KEY)
|
||||
sensor_payload = serializers.JSONField(required=False)
|
||||
plant_ids = serializers.ListField(
|
||||
@@ -40,6 +40,7 @@ class SensorDataUpdateSerializer(serializers.Serializer):
|
||||
known_fields = {
|
||||
"farm_uuid",
|
||||
"farm_boundary",
|
||||
"block_count",
|
||||
"sensor_key",
|
||||
"sensor_payload",
|
||||
"plant_ids",
|
||||
@@ -150,6 +151,8 @@ class FarmCenterLocationSerializer(serializers.Serializer):
|
||||
lat = serializers.DecimalField(max_digits=9, decimal_places=6)
|
||||
lon = serializers.DecimalField(max_digits=9, decimal_places=6)
|
||||
farm_boundary = serializers.JSONField()
|
||||
input_block_count = serializers.IntegerField()
|
||||
block_layout = serializers.JSONField()
|
||||
|
||||
|
||||
class WeatherForecastDetailSerializer(serializers.ModelSerializer):
|
||||
@@ -173,7 +176,7 @@ class WeatherForecastDetailSerializer(serializers.ModelSerializer):
|
||||
class FarmSoilPayloadSerializer(serializers.Serializer):
|
||||
resolved_metrics = serializers.JSONField()
|
||||
metric_sources = serializers.JSONField()
|
||||
depths = SoilDepthDataSerializer(many=True)
|
||||
satellite_snapshots = serializers.JSONField()
|
||||
|
||||
|
||||
class PlantCatalogSnapshotSerializer(serializers.ModelSerializer):
|
||||
|
||||
+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":
|
||||
|
||||
@@ -5,7 +5,7 @@ import uuid
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from location_data.models import SoilDepthData, SoilLocation
|
||||
from location_data.models import BlockSubdivision, SoilLocation
|
||||
from farm_data.models import PlantCatalogSnapshot, SensorData, SensorParameter
|
||||
from farm_data.services import (
|
||||
assign_farm_plants_from_backend_ids,
|
||||
@@ -42,19 +42,6 @@ class FarmDetailApiTests(TestCase):
|
||||
longitude="51.400000",
|
||||
farm_boundary={"type": "Polygon", "coordinates": []},
|
||||
)
|
||||
SoilDepthData.objects.create(
|
||||
soil_location=self.location,
|
||||
depth_label="0-5cm",
|
||||
clay=22.0,
|
||||
nitrogen=10.0,
|
||||
sand=40.0,
|
||||
)
|
||||
SoilDepthData.objects.create(
|
||||
soil_location=self.location,
|
||||
depth_label="5-15cm",
|
||||
clay=18.0,
|
||||
nitrogen=8.0,
|
||||
)
|
||||
self.weather = WeatherForecast.objects.create(
|
||||
location=self.location,
|
||||
forecast_date=date(2026, 4, 10),
|
||||
@@ -123,9 +110,7 @@ class FarmDetailApiTests(TestCase):
|
||||
self.assertEqual(resolved_metrics["nitrogen"], 99.0)
|
||||
self.assertEqual(metric_sources["nitrogen"]["type"], "sensor")
|
||||
self.assertEqual(metric_sources["nitrogen"]["strategy"], "single_value")
|
||||
self.assertEqual(resolved_metrics["clay"], 22.0)
|
||||
self.assertEqual(metric_sources["clay"], "soil")
|
||||
self.assertEqual(len(payload["soil"]["depths"]), 2)
|
||||
self.assertEqual(payload["soil"]["satellite_snapshots"], [])
|
||||
self.assertCountEqual(payload["plant_ids"], [self.plant1.backend_plant_id, self.plant2.backend_plant_id])
|
||||
self.assertEqual(len(payload["plants"]), 2)
|
||||
returned_plants = {item["id"]: item for item in payload["plants"]}
|
||||
@@ -204,21 +189,6 @@ class FarmDataUpsertApiTests(TestCase):
|
||||
latitude="35.710000",
|
||||
longitude="51.410000",
|
||||
)
|
||||
SoilDepthData.objects.create(
|
||||
soil_location=self.location,
|
||||
depth_label="0-5cm",
|
||||
clay=20.0,
|
||||
)
|
||||
SoilDepthData.objects.create(
|
||||
soil_location=self.location,
|
||||
depth_label="5-15cm",
|
||||
clay=18.0,
|
||||
)
|
||||
SoilDepthData.objects.create(
|
||||
soil_location=self.location,
|
||||
depth_label="15-30cm",
|
||||
clay=16.0,
|
||||
)
|
||||
self.boundary = square_boundary_for_center(35.71, 51.41)
|
||||
self.weather = WeatherForecast.objects.create(
|
||||
location=self.location,
|
||||
@@ -312,16 +282,7 @@ class FarmDataUpsertApiTests(TestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("farm_uuid", response.json()["data"])
|
||||
|
||||
@patch("farm_data.services.update_weather_for_location", return_value={"status": "no_data"})
|
||||
@patch(
|
||||
"farm_data.services.fetch_soil_data_for_coordinates",
|
||||
return_value={"status": "completed", "depths": []},
|
||||
)
|
||||
def test_post_creates_center_location_from_boundary_when_missing(
|
||||
self,
|
||||
_mock_fetch_soil_data_for_coordinates,
|
||||
_mock_update_weather_for_location,
|
||||
):
|
||||
def test_post_creates_center_location_from_boundary_when_missing(self):
|
||||
farm_uuid = uuid.uuid4()
|
||||
|
||||
response = self.client.post(
|
||||
@@ -347,6 +308,60 @@ class FarmDataUpsertApiTests(TestCase):
|
||||
self.assertEqual(str(farm.center_location.latitude), "50.010000")
|
||||
self.assertEqual(str(farm.center_location.longitude), "50.010000")
|
||||
self.assertIsNone(farm.weather_forecast_id)
|
||||
self.assertEqual(farm.center_location.input_block_count, 1)
|
||||
self.assertEqual(len(farm.center_location.block_layout["blocks"]), 1)
|
||||
subdivision = BlockSubdivision.objects.get(soil_location=farm.center_location, block_code="block-1")
|
||||
self.assertGreater(subdivision.grid_point_count, 0)
|
||||
self.assertEqual(subdivision.grid_point_count, subdivision.centroid_count)
|
||||
|
||||
def test_post_persists_requested_block_count_on_center_location(self):
|
||||
farm_uuid = uuid.uuid4()
|
||||
|
||||
response = self.client.post(
|
||||
"/api/farm-data/",
|
||||
data={
|
||||
"farm_uuid": str(farm_uuid),
|
||||
"farm_boundary": self.boundary,
|
||||
"block_count": 3,
|
||||
"sensor_payload": {"sensor-7-1": {"soil_moisture": 40.0}},
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
farm = SensorData.objects.get(farm_uuid=farm_uuid)
|
||||
self.assertEqual(farm.center_location.input_block_count, 3)
|
||||
self.assertEqual(len(farm.center_location.block_layout["blocks"]), 3)
|
||||
self.assertFalse(
|
||||
BlockSubdivision.objects.filter(soil_location=farm.center_location).exists()
|
||||
)
|
||||
|
||||
def test_resolve_center_location_runs_subdivision_only_on_creation(self):
|
||||
boundary = square_boundary_for_center(35.75, 51.45)
|
||||
|
||||
first_location = resolve_center_location_from_boundary(boundary, block_count=1)
|
||||
first_subdivision = BlockSubdivision.objects.get(
|
||||
soil_location=first_location,
|
||||
block_code="block-1",
|
||||
)
|
||||
|
||||
second_location = resolve_center_location_from_boundary(boundary, block_count=1)
|
||||
|
||||
self.assertEqual(first_location.id, second_location.id)
|
||||
self.assertEqual(
|
||||
BlockSubdivision.objects.filter(
|
||||
soil_location=second_location,
|
||||
block_code="block-1",
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(
|
||||
BlockSubdivision.objects.get(
|
||||
soil_location=second_location,
|
||||
block_code="block-1",
|
||||
).id,
|
||||
first_subdivision.id,
|
||||
)
|
||||
|
||||
def test_resolve_center_location_uses_geometric_centroid_for_concave_polygon(self):
|
||||
location = resolve_center_location_from_boundary(
|
||||
@@ -368,53 +383,10 @@ class FarmDataUpsertApiTests(TestCase):
|
||||
self.assertEqual(str(location.latitude), "2.078947")
|
||||
self.assertEqual(str(location.longitude), "2.078947")
|
||||
|
||||
@patch("farm_data.services.update_weather_for_location")
|
||||
@patch("farm_data.services.fetch_soil_data_for_coordinates")
|
||||
def test_post_fetches_missing_location_and_weather_data(
|
||||
self,
|
||||
mock_fetch_soil_data_for_coordinates,
|
||||
mock_update_weather_for_location,
|
||||
):
|
||||
def test_post_keeps_missing_location_without_external_sync(self):
|
||||
missing_boundary = square_boundary_for_center(36.0, 52.0)
|
||||
farm_uuid = uuid.uuid4()
|
||||
|
||||
def soil_side_effect(latitude, longitude, task_id="", progress_callback=None):
|
||||
location = SoilLocation.objects.get(
|
||||
latitude="36.000000",
|
||||
longitude="52.000000",
|
||||
)
|
||||
SoilDepthData.objects.update_or_create(
|
||||
soil_location=location,
|
||||
depth_label="0-5cm",
|
||||
defaults={"clay": 20.0},
|
||||
)
|
||||
SoilDepthData.objects.update_or_create(
|
||||
soil_location=location,
|
||||
depth_label="5-15cm",
|
||||
defaults={"clay": 18.0},
|
||||
)
|
||||
SoilDepthData.objects.update_or_create(
|
||||
soil_location=location,
|
||||
depth_label="15-30cm",
|
||||
defaults={"clay": 16.0},
|
||||
)
|
||||
return {"status": "completed", "location_id": location.id, "depths": ["0-5cm", "5-15cm", "15-30cm"]}
|
||||
|
||||
def weather_side_effect(location):
|
||||
WeatherForecast.objects.update_or_create(
|
||||
location=location,
|
||||
forecast_date=date(2026, 4, 12),
|
||||
defaults={
|
||||
"temperature_min": 10.0,
|
||||
"temperature_max": 20.0,
|
||||
"temperature_mean": 15.0,
|
||||
},
|
||||
)
|
||||
return {"status": "success", "location_id": location.id, "days_updated": 1}
|
||||
|
||||
mock_fetch_soil_data_for_coordinates.side_effect = soil_side_effect
|
||||
mock_update_weather_for_location.side_effect = weather_side_effect
|
||||
|
||||
response = self.client.post(
|
||||
"/api/farm-data/",
|
||||
data={
|
||||
@@ -426,9 +398,5 @@ class FarmDataUpsertApiTests(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
mock_fetch_soil_data_for_coordinates.assert_called_once()
|
||||
mock_update_weather_for_location.assert_called_once()
|
||||
|
||||
farm = SensorData.objects.get(farm_uuid=farm_uuid)
|
||||
self.assertEqual(farm.center_location.depths.count(), 3)
|
||||
self.assertIsNotNone(farm.weather_forecast_id)
|
||||
self.assertIsNone(farm.weather_forecast_id)
|
||||
|
||||
+8
-1
@@ -83,6 +83,7 @@ class FarmDataUpsertView(APIView):
|
||||
"`farm_uuid` باید از API ارسال شود و هرگز خودکار ساخته نمیشود. "
|
||||
"مرز مزرعه را میگیرد، مرکز زمین را خودش محاسبه و در location_data ذخیره میکند. "
|
||||
"رکورد آبوهوا هم از همان مرکز زمین بهصورت خودکار پیدا میشود. "
|
||||
"در این مرحله برای location_data هیچ ریکوئست خارجی برای بلوکها زده نمیشود. "
|
||||
'خوانشها داخل `sensor_payload` مثل `{"sensor-7-1": {...}}` نگهداری میشوند.'
|
||||
),
|
||||
request=SensorDataUpdateSerializer,
|
||||
@@ -121,6 +122,7 @@ class FarmDataUpsertView(APIView):
|
||||
]
|
||||
],
|
||||
},
|
||||
"block_count": 3,
|
||||
"sensor_payload": {
|
||||
"sensor-7-1": {
|
||||
"soil_moisture": 45.2,
|
||||
@@ -147,6 +149,7 @@ class FarmDataUpsertView(APIView):
|
||||
{"lat": 35.7200, "lon": 51.3900},
|
||||
]
|
||||
},
|
||||
"block_count": 2,
|
||||
"sensor_payload": {
|
||||
"sensor-7-1": {
|
||||
"soil_moisture": 45.2,
|
||||
@@ -172,11 +175,15 @@ class FarmDataUpsertView(APIView):
|
||||
|
||||
farm_uuid = serializer.validated_data["farm_uuid"]
|
||||
farm_boundary = serializer.validated_data["farm_boundary"]
|
||||
block_count = serializer.validated_data.get("block_count", 1)
|
||||
plant_ids = serializer.validated_data.get("plant_ids")
|
||||
irrigation_method_id = serializer.validated_data.get("irrigation_method_id")
|
||||
sensor_payload = serializer.validated_data.get("sensor_payload", {})
|
||||
try:
|
||||
center_location = resolve_center_location_from_boundary(farm_boundary)
|
||||
center_location = resolve_center_location_from_boundary(
|
||||
farm_boundary,
|
||||
block_count=block_count,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return Response(
|
||||
{"code": 400, "msg": "داده نامعتبر.", "data": {"farm_boundary": [str(exc)]}},
|
||||
|
||||
Reference in New Issue
Block a user