Files
Ai/farm_data/views.py
T

432 lines
16 KiB
Python
Raw Normal View History

2026-04-06 23:50:24 +03:30
from copy import deepcopy
from django.db import transaction
from drf_spectacular.utils import (
OpenApiExample,
OpenApiResponse,
extend_schema,
inline_serializer,
)
from rest_framework import serializers as drf_serializers
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from config.openapi import build_envelope_serializer, build_response
from .models import ParameterUpdateLog, SensorData, SensorParameter
from .serializers import (
FarmDetailSerializer,
SensorDataResponseSerializer,
SensorDataUpdateSerializer,
SensorParameterSerializer,
)
from .services import (
2026-05-05 01:46:10 +03:30
BackendSyncError,
assign_farm_plants_from_backend_ids,
2026-04-07 01:08:41 +03:30
ExternalDataSyncError,
ensure_location_and_weather_data,
2026-04-06 23:50:24 +03:30
get_farm_details,
resolve_center_location_from_boundary,
2026-05-05 01:46:10 +03:30
sync_sensor_parameters_from_payload,
sync_plant_catalog_from_backend,
2026-04-06 23:50:24 +03:30
)
SensorDataEnvelopeSerializer = build_envelope_serializer(
"SensorDataEnvelopeSerializer",
SensorDataResponseSerializer,
)
SensorDataValidationErrorSerializer = build_envelope_serializer(
"SensorDataValidationErrorSerializer",
data_required=False,
allow_null=True,
)
SensorDataNotFoundSerializer = build_envelope_serializer(
"SensorDataNotFoundSerializer",
data_required=False,
allow_null=True,
)
FarmDetailEnvelopeSerializer = build_envelope_serializer(
"FarmDetailEnvelopeSerializer",
FarmDetailSerializer,
)
SensorParameterResponseSerializer = build_envelope_serializer(
"SensorParameterEnvelopeSerializer",
inline_serializer(
name="SensorParameterPayloadSerializer",
fields={
"id": drf_serializers.IntegerField(),
"sensor_key": drf_serializers.CharField(),
"code": drf_serializers.CharField(),
"name_fa": drf_serializers.CharField(),
"unit": drf_serializers.CharField(),
"data_type": drf_serializers.CharField(),
"metadata": drf_serializers.JSONField(),
"created_at": drf_serializers.DateTimeField(),
"action": drf_serializers.CharField(),
},
),
)
class FarmDataUpsertView(APIView):
"""
ایجاد یا آپدیت داده farm.
"""
@extend_schema(
tags=["Farm Data"],
summary="ایجاد یا آپدیت داده farm",
description=(
"داده farm را با `POST /api/farm-data/` ایجاد یا آپدیت می‌کند. "
"`farm_uuid` باید از API ارسال شود و هرگز خودکار ساخته نمی‌شود. "
"مرز مزرعه را می‌گیرد، مرکز زمین را خودش محاسبه و در location_data ذخیره می‌کند. "
"رکورد آب‌وهوا هم از همان مرکز زمین به‌صورت خودکار پیدا می‌شود. "
'خوانش‌ها داخل `sensor_payload` مثل `{"sensor-7-1": {...}}` نگه‌داری می‌شوند.'
),
request=SensorDataUpdateSerializer,
responses={
200: build_response(
SensorDataEnvelopeSerializer,
"داده farm با موفقیت به‌روزرسانی شد.",
),
201: build_response(
SensorDataEnvelopeSerializer,
"داده farm با موفقیت ایجاد شد.",
),
400: build_response(
SensorDataValidationErrorSerializer,
"داده ورودی نامعتبر است.",
),
2026-04-07 01:08:41 +03:30
502: build_response(
SensorDataNotFoundSerializer,
"واکشی داده خاک یا آب‌وهوا از سرویس بیرونی ناموفق بود.",
),
2026-04-06 23:50:24 +03:30
},
examples=[
OpenApiExample(
"نمونه درخواست",
value={
"farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
"farm_boundary": {
"type": "Polygon",
"coordinates": [
[
[51.3900, 35.7000],
[51.4100, 35.7000],
[51.4100, 35.7200],
[51.3900, 35.7200],
[51.3900, 35.7000],
]
],
},
"sensor_payload": {
"sensor-7-1": {
"soil_moisture": 45.2,
"soil_temperature": 22.5,
"soil_ph": 6.8,
"electrical_conductivity": 1.2,
"nitrogen": 30.0,
"phosphorus": 15.0,
"potassium": 20.0,
}
},
},
request_only=True,
),
OpenApiExample(
"نمونه چند سنسور",
value={
"farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
"farm_boundary": {
"corners": [
{"lat": 35.7000, "lon": 51.3900},
{"lat": 35.7000, "lon": 51.4100},
{"lat": 35.7200, "lon": 51.4100},
{"lat": 35.7200, "lon": 51.3900},
]
},
"sensor_payload": {
"sensor-7-1": {
"soil_moisture": 45.2,
"soil_temperature": 22.5,
},
"leaf-sensor": {
"leaf_wetness": 11.0,
"leaf_temperature": 19.3,
},
},
},
request_only=True,
),
],
)
def post(self, request):
serializer = SensorDataUpdateSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
farm_uuid = serializer.validated_data["farm_uuid"]
farm_boundary = serializer.validated_data["farm_boundary"]
plant_ids = serializer.validated_data.get("plant_ids")
2026-04-24 02:50:27 +03:30
irrigation_method_id = serializer.validated_data.get("irrigation_method_id")
2026-04-06 23:50:24 +03:30
sensor_payload = serializer.validated_data.get("sensor_payload", {})
try:
center_location = resolve_center_location_from_boundary(farm_boundary)
except ValueError as exc:
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": {"farm_boundary": [str(exc)]}},
status=status.HTTP_400_BAD_REQUEST,
)
2026-04-07 01:08:41 +03:30
try:
center_location, weather_forecast = ensure_location_and_weather_data(
center_location
)
except ExternalDataSyncError as exc:
return Response(
{"code": 502, "msg": str(exc), "data": None},
status=status.HTTP_502_BAD_GATEWAY,
)
2026-04-06 23:50:24 +03:30
with transaction.atomic():
2026-05-05 01:46:10 +03:30
sync_sensor_parameters_from_payload(sensor_payload)
2026-04-06 23:50:24 +03:30
farm_data, created = SensorData.objects.get_or_create(
farm_uuid=farm_uuid,
defaults={
"center_location": center_location,
"weather_forecast": weather_forecast,
"sensor_payload": sensor_payload,
},
)
if not created and sensor_payload:
merged_payload = deepcopy(farm_data.sensor_payload or {})
for sensor_key, sensor_values in sensor_payload.items():
current_values = merged_payload.get(sensor_key, {})
if not isinstance(current_values, dict):
current_values = {}
current_values.update(sensor_values)
merged_payload[sensor_key] = current_values
farm_data.sensor_payload = merged_payload
elif created:
farm_data.sensor_payload = sensor_payload
farm_data.center_location = center_location
farm_data.weather_forecast = weather_forecast
2026-04-24 02:50:27 +03:30
if "irrigation_method_id" in serializer.validated_data:
farm_data.irrigation_method_id = irrigation_method_id
2026-04-06 23:50:24 +03:30
if not created:
farm_data.save(
update_fields=[
"center_location",
"weather_forecast",
"sensor_payload",
2026-04-24 02:50:27 +03:30
"irrigation_method",
2026-04-06 23:50:24 +03:30
"updated_at",
]
)
else:
farm_data.save()
if plant_ids is not None:
2026-05-05 01:46:10 +03:30
try:
assign_farm_plants_from_backend_ids(farm_data, plant_ids)
except BackendSyncError as exc:
return Response(
{"code": 400, "msg": str(exc), "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
2026-04-06 23:50:24 +03:30
response_status = (
status.HTTP_201_CREATED if created else status.HTTP_200_OK
)
return Response(
{
"code": 201 if created else 200,
"msg": "success",
"data": SensorDataResponseSerializer(farm_data).data,
},
status=response_status,
)
class FarmDetailView(APIView):
@extend_schema(
tags=["Farm Data"],
summary="دریافت همه اطلاعات farm",
description=(
"اطلاعات تجمیعی farm را برمی‌گرداند. "
2026-04-25 17:22:41 +03:30
"برای resolved_metrics، داده‌های sensor روی داده‌های خاک اولویت دارند "
"و در حالت چند سنسوره، مقادیر متعارض به‌صورت deterministic تجمیع می‌شوند."
2026-04-06 23:50:24 +03:30
),
responses={
200: build_response(
FarmDetailEnvelopeSerializer,
"اطلاعات farm با موفقیت بازگردانده شد.",
),
404: build_response(
SensorDataNotFoundSerializer,
"farm موردنظر یافت نشد.",
),
},
)
def get(self, request, farm_uuid):
data = get_farm_details(str(farm_uuid))
if data is None:
return Response(
{"code": 404, "msg": "farm یافت نشد.", "data": None},
status=status.HTTP_404_NOT_FOUND,
)
return Response(
{"code": 200, "msg": "success", "data": data},
status=status.HTTP_200_OK,
)
2026-05-05 01:46:10 +03:30
class PlantCatalogSyncView(APIView):
@extend_schema(
tags=["Farm Data"],
summary="همگام‌سازی کاتالوگ گیاه از Backend",
description="payload گیاه‌های canonical را از Backend دریافت و در `farm_data` snapshot می‌کند.",
request=drf_serializers.ListSerializer(
child=inline_serializer(
name="PlantCatalogSyncItem",
fields={
"id": drf_serializers.IntegerField(),
"name": drf_serializers.CharField(),
},
)
),
responses={
200: OpenApiResponse(description="کاتالوگ گیاه با موفقیت sync شد."),
400: OpenApiResponse(description="payload نامعتبر است."),
},
)
def post(self, request):
if not isinstance(request.data, list):
return Response(
{"code": 400, "msg": "payload باید آرایه‌ای از گیاه‌ها باشد.", "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
try:
snapshots = sync_plant_catalog_from_backend(request.data)
except BackendSyncError as exc:
return Response(
{"code": 400, "msg": str(exc), "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
return Response(
{
"code": 200,
"msg": "success",
"data": {
"count": len(snapshots),
"plant_ids": [snapshot.backend_plant_id for snapshot in snapshots],
},
},
status=status.HTTP_200_OK,
)
2026-04-06 23:50:24 +03:30
class SensorParameterCreateView(APIView):
"""
اضافه کردن پارامتر جدید و ثبت در ParameterUpdateLog.
"""
@extend_schema(
tags=["Farm Parameters"],
summary="افزودن/ویرایش پارامتر سنسور",
description="پارامتر جدید اضافه یا پارامتر موجود را ویرایش می‌کند و در لاگ ثبت می‌شود.",
request=SensorParameterSerializer,
responses={
201: build_response(
SensorParameterResponseSerializer,
"پارامتر سنسور با موفقیت ایجاد یا ویرایش شد.",
),
400: build_response(
SensorDataValidationErrorSerializer,
"داده ورودی نامعتبر است.",
),
},
examples=[
OpenApiExample(
"نمونه درخواست",
value={
"sensor_key": "sensor-7-1",
"code": "soil_moisture",
"name_fa": "رطوبت خاک",
"unit": "%",
"data_type": "float",
"metadata": {"min": 0, "max": 100},
},
request_only=True,
),
],
)
def post(self, request):
serializer = SensorParameterSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
sensor_key = serializer.validated_data.get("sensor_key")
code = serializer.validated_data["code"]
name_fa = serializer.validated_data["name_fa"]
unit = serializer.validated_data.get("unit", "")
data_type = serializer.validated_data.get("data_type", "")
metadata = serializer.validated_data.get("metadata", {})
with transaction.atomic():
parameter, created = SensorParameter.objects.update_or_create(
sensor_key=sensor_key,
code=code,
defaults={
"name_fa": name_fa,
"unit": unit,
"data_type": data_type,
"metadata": metadata,
},
)
action = (
ParameterUpdateLog.ACTION_ADDED
if created
else ParameterUpdateLog.ACTION_MODIFIED
)
ParameterUpdateLog.objects.create(
parameter=parameter,
action=action,
payload={
"sensor_key": parameter.sensor_key,
"code": parameter.code,
"name_fa": parameter.name_fa,
"unit": parameter.unit,
"data_type": parameter.data_type,
"metadata": parameter.metadata,
},
)
return Response(
{
"code": 201,
"msg": "success",
"data": {
"id": parameter.id,
"sensor_key": parameter.sensor_key,
"code": parameter.code,
"name_fa": parameter.name_fa,
"unit": parameter.unit,
"data_type": parameter.data_type,
"metadata": parameter.metadata,
"created_at": parameter.created_at,
"action": action,
},
},
status=status.HTTP_201_CREATED,
)