UPDATE
This commit is contained in:
@@ -1,260 +0,0 @@
|
|||||||
# مستندات جداول پایگاه داده — CropLogic AI
|
|
||||||
|
|
||||||
این سند تمام جداول (مدلهای Django) موجود در پروژه را به همراه توضیح ستونها و روابط بین آنها شرح میدهد.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## فهرست جداول
|
|
||||||
|
|
||||||
| اپ | جدول | توضیح کوتاه |
|
|
||||||
|---|---|---|
|
|
||||||
| `location_data` | `SoilLocation` | موقعیت جغرافیایی (lat/lon) |
|
|
||||||
| `location_data` | `SoilDepthData` | دادههای خاک به تفکیک عمق |
|
|
||||||
| `sensor_data` | `SensorData` | آخرین خوانش سنسور برای یک موقعیت |
|
|
||||||
| `sensor_data` | `SensorDataHistory` | تاریخچه خوانشهای سنسور |
|
|
||||||
| `sensor_data` | `SensorParameter` | تعریف پارامترهای سنسور |
|
|
||||||
| `sensor_data` | `ParameterUpdateLog` | لاگ تغییرات پارامترهای سنسور |
|
|
||||||
| `weather` | `WeatherParameter` | تعریف پارامترهای هواشناسی |
|
|
||||||
| `weather` | `WeatherForecast` | پیشبینی آبوهوای روزانه |
|
|
||||||
| `plant` | `Plant` | اطلاعات گیاهان |
|
|
||||||
| `irrigation` | `IrrigationMethod` | روشهای آبیاری |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## اپ: `location_data`
|
|
||||||
|
|
||||||
### جدول `SoilLocation`
|
|
||||||
|
|
||||||
موقعیتهای جغرافیایی که دادههای خاک و سنسور به آنها متصل هستند.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | PK (auto) | شناسه اتوماتیک |
|
|
||||||
| `latitude` | DecimalField (9,6) | عرض جغرافیایی |
|
|
||||||
| `longitude` | DecimalField (9,6) | طول جغرافیایی |
|
|
||||||
| `task_id` | CharField | شناسه تسک Celery در حال پردازش |
|
|
||||||
| `created_at` | DateTimeField | زمان ایجاد |
|
|
||||||
| `updated_at` | DateTimeField | آخرین زمان بهروزرسانی |
|
|
||||||
|
|
||||||
**محدودیتها:**
|
|
||||||
- ترکیب `(latitude, longitude)` باید یکتا باشد.
|
|
||||||
|
|
||||||
**روابط:**
|
|
||||||
- ← `SoilDepthData.soil_location` (یک به چند)
|
|
||||||
- ← `SensorData.location` (یک به چند)
|
|
||||||
- ← `WeatherForecast.location` (یک به چند)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### جدول `SoilDepthData`
|
|
||||||
|
|
||||||
دادههای خاک از API SoilGrids برای سه عمق مختلف، مرتبط با هر `SoilLocation`.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | PK (auto) | شناسه اتوماتیک |
|
|
||||||
| `soil_location` | FK → SoilLocation | موقعیت مکانی مرتبط |
|
|
||||||
| `depth_label` | CharField | عمق: `0-5cm` / `5-15cm` / `15-30cm` |
|
|
||||||
| `bdod` | FloatField | چگالی ظاهری خاک (Bulk Density) |
|
|
||||||
| `cec` | FloatField | ظرفیت تبادل کاتیونی (CEC) |
|
|
||||||
| `cfvo` | FloatField | درصد حجمی سنگریزه |
|
|
||||||
| `clay` | FloatField | درصد رس |
|
|
||||||
| `nitrogen` | FloatField | نیتروژن کل |
|
|
||||||
| `ocd` | FloatField | تراکم کربن آلی |
|
|
||||||
| `ocs` | FloatField | ذخیره کربن آلی |
|
|
||||||
| `phh2o` | FloatField | pH خاک در آب |
|
|
||||||
| `sand` | FloatField | درصد شن |
|
|
||||||
| `silt` | FloatField | درصد سیلت |
|
|
||||||
| `soc` | FloatField | کربن آلی خاک (SOC) |
|
|
||||||
| `wv0010` | FloatField | رطوبت حجمی در ۱۰ kPa |
|
|
||||||
| `wv0033` | FloatField | ظرفیت زراعی — رطوبت در ۳۳ kPa |
|
|
||||||
| `wv1500` | FloatField | نقطه پژمردگی — رطوبت در ۱۵۰۰ kPa |
|
|
||||||
| `created_at` | DateTimeField | زمان ایجاد |
|
|
||||||
|
|
||||||
**محدودیتها:**
|
|
||||||
- ترکیب `(soil_location, depth_label)` باید یکتا باشد.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## اپ: `sensor_data`
|
|
||||||
|
|
||||||
### جدول `SensorData`
|
|
||||||
|
|
||||||
آخرین خوانش سنسور فیزیکی برای یک موقعیت. هنگام بهروزرسانی، نسخه قبلی به `SensorDataHistory` منتقل میشود.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `uuid_sensor` | UUIDField (PK) | شناسه یکتای سنسور |
|
|
||||||
| `location` | FK → SoilLocation | موقعیت مکانی (ستون DB: `location_id`) |
|
|
||||||
| `soil_moisture` | FloatField | رطوبت خاک |
|
|
||||||
| `soil_temperature` | FloatField | دمای خاک |
|
|
||||||
| `soil_ph` | FloatField | pH خاک |
|
|
||||||
| `electrical_conductivity` | FloatField | هدایت الکتریکی (EC) |
|
|
||||||
| `nitrogen` | FloatField | ازت (N) |
|
|
||||||
| `phosphorus` | FloatField | فسفر (P) |
|
|
||||||
| `potassium` | FloatField | پتاسیم (K) |
|
|
||||||
| `plants` | M2M → Plant | گیاهان مرتبط با این سنسور |
|
|
||||||
| `created_at` | DateTimeField | زمان ایجاد |
|
|
||||||
| `updated_at` | DateTimeField | آخرین زمان بهروزرسانی |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### جدول `SensorDataHistory`
|
|
||||||
|
|
||||||
تاریخچه کامل خوانشهای سنسور. هر بار که `SensorData` بهروز میشود، نسخه قبلی اینجا ذخیره میشود.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | PK (auto) | شناسه اتوماتیک |
|
|
||||||
| `uuid_sensor` | UUIDField | شناسه سنسور اصلی |
|
|
||||||
| `location_id` | IntegerField | شناسه موقعیت مکانی |
|
|
||||||
| `soil_moisture` | FloatField | رطوبت خاک |
|
|
||||||
| `soil_temperature` | FloatField | دمای خاک |
|
|
||||||
| `soil_ph` | FloatField | pH خاک |
|
|
||||||
| `electrical_conductivity` | FloatField | هدایت الکتریکی |
|
|
||||||
| `nitrogen` | FloatField | ازت |
|
|
||||||
| `phosphorus` | FloatField | فسفر |
|
|
||||||
| `potassium` | FloatField | پتاسیم |
|
|
||||||
| `recorded_at` | DateTimeField | زمان ثبت در تاریخچه |
|
|
||||||
|
|
||||||
> **نکته:** این جدول FK مستقیم به SoilLocation ندارد تا در صورت حذف موقعیت، تاریخچه حفظ شود.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### جدول `SensorParameter`
|
|
||||||
|
|
||||||
کاتالوگ پارامترهای قابل اندازهگیری توسط سنسورها.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | PK (auto) | شناسه اتوماتیک |
|
|
||||||
| `code` | CharField (unique) | کد یکتا (مثال: `soil_moisture`) |
|
|
||||||
| `name_fa` | CharField | نام فارسی پارامتر |
|
|
||||||
| `unit` | CharField | واحد اندازهگیری |
|
|
||||||
| `created_at` | DateTimeField | زمان ایجاد |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### جدول `ParameterUpdateLog`
|
|
||||||
|
|
||||||
لاگ تغییرات (افزودن یا ویرایش) پارامترهای سنسور.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | PK (auto) | شناسه اتوماتیک |
|
|
||||||
| `parameter` | FK → SensorParameter | پارامتر مرتبط |
|
|
||||||
| `action` | CharField | نوع عملیات: `added` یا `modified` |
|
|
||||||
| `updated_at` | DateTimeField | زمان ثبت لاگ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## اپ: `weather`
|
|
||||||
|
|
||||||
### جدول `WeatherParameter`
|
|
||||||
|
|
||||||
کاتالوگ پارامترهای هواشناسی تعریفشده در سیستم.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | PK (auto) | شناسه اتوماتیک |
|
|
||||||
| `code` | CharField (unique) | کد یکتا (مثال: `temperature_max`) |
|
|
||||||
| `name_fa` | CharField | نام فارسی پارامتر |
|
|
||||||
| `unit` | CharField | واحد اندازهگیری |
|
|
||||||
| `created_at` | DateTimeField | زمان ایجاد |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### جدول `WeatherForecast`
|
|
||||||
|
|
||||||
پیشبینی روزانه آبوهوا (تا ۷ روز آینده) برای هر موقعیت مکانی.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | PK (auto) | شناسه اتوماتیک |
|
|
||||||
| `location` | FK → SoilLocation | موقعیت مکانی مرتبط |
|
|
||||||
| `forecast_date` | DateField | تاریخ پیشبینی |
|
|
||||||
| `temperature_min` | FloatField | حداقل دمای هوا (°C) |
|
|
||||||
| `temperature_max` | FloatField | حداکثر دمای هوا (°C) |
|
|
||||||
| `temperature_mean` | FloatField | میانگین دمای هوا (°C) |
|
|
||||||
| `precipitation` | FloatField | مجموع بارش (mm) |
|
|
||||||
| `precipitation_probability` | FloatField | احتمال بارش (%) |
|
|
||||||
| `humidity_mean` | FloatField | میانگین رطوبت نسبی (%) |
|
|
||||||
| `wind_speed_max` | FloatField | حداکثر سرعت باد (km/h) |
|
|
||||||
| `et0` | FloatField | تبخیر-تعرق مرجع ET₀ (mm/day) |
|
|
||||||
| `weather_code` | IntegerField | کد وضعیت آبوهوا (WMO) |
|
|
||||||
| `fetched_at` | DateTimeField | آخرین زمان واکشی از API |
|
|
||||||
| `created_at` | DateTimeField | زمان ایجاد |
|
|
||||||
|
|
||||||
**محدودیتها:**
|
|
||||||
- ترکیب `(location, forecast_date)` باید یکتا باشد.
|
|
||||||
|
|
||||||
**Property:**
|
|
||||||
- `will_rain` → `True` اگر `precipitation > 0`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## اپ: `plant`
|
|
||||||
|
|
||||||
### جدول `Plant`
|
|
||||||
|
|
||||||
اطلاعات گیاهان شامل شرایط کاشت، نگهداری و برداشت.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | PK (auto) | شناسه اتوماتیک |
|
|
||||||
| `name` | CharField (unique) | نام گیاه |
|
|
||||||
| `light` | CharField | نور مورد نیاز |
|
|
||||||
| `watering` | CharField | نیاز آبیاری |
|
|
||||||
| `soil` | CharField | نوع خاک مناسب |
|
|
||||||
| `temperature` | CharField | دمای مناسب رشد |
|
|
||||||
| `planting_season` | CharField | فصل کاشت |
|
|
||||||
| `harvest_time` | CharField | زمان برداشت |
|
|
||||||
| `spacing` | CharField | فاصله کاشت |
|
|
||||||
| `fertilizer` | CharField | کود مناسب |
|
|
||||||
| `created_at` | DateTimeField | زمان ایجاد |
|
|
||||||
| `updated_at` | DateTimeField | آخرین زمان بهروزرسانی |
|
|
||||||
|
|
||||||
**روابط:**
|
|
||||||
- ← `SensorData.plants` (M2M از طریق جدول واسط)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## اپ: `irrigation`
|
|
||||||
|
|
||||||
### جدول `IrrigationMethod`
|
|
||||||
|
|
||||||
مشخصات فنی روشهای مختلف آبیاری.
|
|
||||||
|
|
||||||
| ستون | نوع | توضیح |
|
|
||||||
|---|---|---|
|
|
||||||
| `id` | PK (auto) | شناسه اتوماتیک |
|
|
||||||
| `name` | CharField (unique) | نام روش آبیاری |
|
|
||||||
| `category` | CharField | دستهبندی (موضعی / تحت فشار / سطحی) |
|
|
||||||
| `description` | TextField | توضیحات کامل |
|
|
||||||
| `water_efficiency_percent` | FloatField | راندمان مصرف آب (%) |
|
|
||||||
| `water_pressure_required` | CharField | فشار مورد نیاز |
|
|
||||||
| `flow_rate` | CharField | دبی / میزان جریان آب |
|
|
||||||
| `coverage_area` | CharField | مساحت قابل پوشش |
|
|
||||||
| `soil_type` | CharField | نوع خاک مناسب |
|
|
||||||
| `climate_suitability` | CharField | اقلیم مناسب |
|
|
||||||
| `created_at` | DateTimeField | زمان ایجاد |
|
|
||||||
| `updated_at` | DateTimeField | آخرین زمان بهروزرسانی |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## نمودار روابط (خلاصه)
|
|
||||||
|
|
||||||
```
|
|
||||||
SoilLocation
|
|
||||||
├── SoilDepthData (1:N — depth_label: 0-5cm, 5-15cm, 15-30cm)
|
|
||||||
├── SensorData (1:N — uuid_sensor PK)
|
|
||||||
│ └── Plant (M:N — جدول واسط)
|
|
||||||
└── WeatherForecast (1:N — یکتا per location+date)
|
|
||||||
|
|
||||||
SensorParameter
|
|
||||||
└── ParameterUpdateLog (1:N — action: added/modified)
|
|
||||||
|
|
||||||
Plant (مستقل — از طریق M2M به SensorData متصل)
|
|
||||||
IrrigationMethod (مستقل — بدون FK)
|
|
||||||
WeatherParameter (مستقل — کاتالوگ)
|
|
||||||
```
|
|
||||||
+3
-3
@@ -28,7 +28,7 @@ INSTALLED_APPS = [
|
|||||||
"rag",
|
"rag",
|
||||||
"tasks",
|
"tasks",
|
||||||
"location_data",
|
"location_data",
|
||||||
"sensor_data",
|
"farm_data.apps.FarmDataConfig",
|
||||||
"weather",
|
"weather",
|
||||||
"plant",
|
"plant",
|
||||||
"irrigation",
|
"irrigation",
|
||||||
@@ -125,8 +125,8 @@ SPECTACULAR_SETTINGS = {
|
|||||||
{"name": "RAG Recommendations", "description": "توصیههای آبیاری و کودهی مبتنی بر RAG"},
|
{"name": "RAG Recommendations", "description": "توصیههای آبیاری و کودهی مبتنی بر RAG"},
|
||||||
{"name": "Tasks", "description": "مدیریت تسکهای Celery"},
|
{"name": "Tasks", "description": "مدیریت تسکهای Celery"},
|
||||||
{"name": "Soil Data", "description": "دادههای خاک (SoilGrids)"},
|
{"name": "Soil Data", "description": "دادههای خاک (SoilGrids)"},
|
||||||
{"name": "Sensor Data", "description": "دادههای سنسور"},
|
{"name": "Farm Data", "description": "دادههای مزرعه و سنسورها"},
|
||||||
{"name": "Sensor Parameters", "description": "پارامترهای سنسور"},
|
{"name": "Farm Parameters", "description": "پارامترهای سنسورهای مزرعه"},
|
||||||
{"name": "Plant", "description": "مدیریت گیاهان و دریافت اطلاعات گیاه"},
|
{"name": "Plant", "description": "مدیریت گیاهان و دریافت اطلاعات گیاه"},
|
||||||
{"name": "Irrigation", "description": "مدیریت روشهای آبیاری"},
|
{"name": "Irrigation", "description": "مدیریت روشهای آبیاری"},
|
||||||
{"name": "Irrigation Recommendation", "description": "درخواست و پیگیری توصیه آبیاری"},
|
{"name": "Irrigation Recommendation", "description": "درخواست و پیگیری توصیه آبیاری"},
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ urlpatterns = [
|
|||||||
path("api/dashboard-data/", include("dashboard_data.urls")),
|
path("api/dashboard-data/", include("dashboard_data.urls")),
|
||||||
path("api/tasks/", include("tasks.urls")),
|
path("api/tasks/", include("tasks.urls")),
|
||||||
path("api/soil-data/", include("location_data.urls")),
|
path("api/soil-data/", include("location_data.urls")),
|
||||||
path("api/sensor-data/", include("sensor_data.urls")),
|
path("api/farm-data/", include("farm_data.urls")),
|
||||||
path("api/plants/", include("plant.urls")),
|
path("api/plants/", include("plant.urls")),
|
||||||
path("api/irrigation/", include("irrigation.urls")),
|
path("api/irrigation/", include("irrigation.urls")),
|
||||||
path("api/fertilization/", include("fertilization.urls")),
|
path("api/fertilization/", include("fertilization.urls")),
|
||||||
|
|||||||
@@ -41,16 +41,6 @@ def _normalize_to_ideal_score(value: float | None, minimum: float, ideal: float,
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_profile(context: dict[str, Any]) -> tuple[dict[str, dict[str, float]], str]:
|
def _resolve_profile(context: dict[str, Any]) -> tuple[dict[str, dict[str, float]], str]:
|
||||||
location = context.get("location")
|
|
||||||
if location is not None:
|
|
||||||
location_profile = getattr(location, "ideal_sensor_profile", None) or {}
|
|
||||||
if location_profile:
|
|
||||||
merged = {
|
|
||||||
metric: {**DEFAULT_IDEAL_SENSOR_PROFILE.get(metric, {}), **location_profile.get(metric, {})}
|
|
||||||
for metric in set(DEFAULT_IDEAL_SENSOR_PROFILE) | set(location_profile)
|
|
||||||
}
|
|
||||||
return merged, "location"
|
|
||||||
|
|
||||||
plants = context.get("plants", [])
|
plants = context.get("plants", [])
|
||||||
for plant in plants:
|
for plant in plants:
|
||||||
plant_profile = getattr(plant, "health_profile", None) or {}
|
plant_profile = getattr(plant, "health_profile", None) or {}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from math import sqrt
|
from math import sqrt
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sensor_data.models import SensorData, SensorDataHistory
|
from farm_data.models import SensorData
|
||||||
|
|
||||||
|
|
||||||
QUALITY_REAL = "REAL"
|
QUALITY_REAL = "REAL"
|
||||||
@@ -61,9 +61,9 @@ def _latest_sensor_measurement(sensor: Any, histories: list[Any]) -> dict[str, A
|
|||||||
series = _sensor_time_series(sensor, histories)
|
series = _sensor_time_series(sensor, histories)
|
||||||
latest = series[-1] if series else {"timestamp": None, "value": None, "quality_flag": QUALITY_MISSING}
|
latest = series[-1] if series else {"timestamp": None, "value": None, "quality_flag": QUALITY_MISSING}
|
||||||
return {
|
return {
|
||||||
"sensor_id": str(sensor.uuid_sensor),
|
"sensor_id": str(sensor.farm_uuid),
|
||||||
"latitude": float(sensor.location.latitude),
|
"latitude": float(sensor.center_location.latitude),
|
||||||
"longitude": float(sensor.location.longitude),
|
"longitude": float(sensor.center_location.longitude),
|
||||||
"depth": None,
|
"depth": None,
|
||||||
"timestamp": latest["timestamp"],
|
"timestamp": latest["timestamp"],
|
||||||
"soil_moisture_value": latest["value"],
|
"soil_moisture_value": latest["value"],
|
||||||
@@ -99,7 +99,7 @@ def _grid_axis(min_value: float, max_value: float) -> list[float]:
|
|||||||
|
|
||||||
def _load_sensor_network(current_sensor: Any) -> list[Any]:
|
def _load_sensor_network(current_sensor: Any) -> list[Any]:
|
||||||
plant_ids = list(current_sensor.plants.values_list("id", flat=True))
|
plant_ids = list(current_sensor.plants.values_list("id", flat=True))
|
||||||
queryset = SensorData.objects.select_related("location").prefetch_related("plants")
|
queryset = SensorData.objects.select_related("center_location").prefetch_related("plants")
|
||||||
if plant_ids:
|
if plant_ids:
|
||||||
queryset = queryset.filter(plants__id__in=plant_ids).distinct()
|
queryset = queryset.filter(plants__id__in=plant_ids).distinct()
|
||||||
return list(queryset)
|
return list(queryset)
|
||||||
@@ -118,14 +118,8 @@ def build_soil_moisture_heatmap(sensor_id: str, context: dict | None = None, ai_
|
|||||||
}
|
}
|
||||||
|
|
||||||
sensors = _load_sensor_network(current_sensor)
|
sensors = _load_sensor_network(current_sensor)
|
||||||
sensor_ids = [sensor.uuid_sensor for sensor in sensors]
|
|
||||||
history_rows = SensorDataHistory.objects.filter(uuid_sensor__in=sensor_ids).order_by("-recorded_at")[:200]
|
|
||||||
history_map: dict[Any, list[Any]] = {}
|
|
||||||
for row in history_rows:
|
|
||||||
history_map.setdefault(row.uuid_sensor, []).append(row)
|
|
||||||
|
|
||||||
sensor_points = [
|
sensor_points = [
|
||||||
_latest_sensor_measurement(sensor, history_map.get(sensor.uuid_sensor, []))
|
_latest_sensor_measurement(sensor, [])
|
||||||
for sensor in sensors
|
for sensor in sensors
|
||||||
]
|
]
|
||||||
valid_sensor_points = [point for point in sensor_points if point["soil_moisture_value"] is not None]
|
valid_sensor_points = [point for point in sensor_points if point["soil_moisture_value"] is not None]
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ from datetime import date
|
|||||||
def load_dashboard_context(sensor_id: str) -> dict | None:
|
def load_dashboard_context(sensor_id: str) -> dict | None:
|
||||||
from irrigation.models import IrrigationMethod
|
from irrigation.models import IrrigationMethod
|
||||||
from location_data.models import SoilDepthData
|
from location_data.models import SoilDepthData
|
||||||
from sensor_data.models import SensorData, SensorDataHistory
|
from farm_data.models import SensorData
|
||||||
from weather.models import WeatherForecast
|
from weather.models import WeatherForecast
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sensor = SensorData.objects.select_related("location").prefetch_related("plants").get(
|
sensor = SensorData.objects.select_related("center_location").prefetch_related("plants").get(
|
||||||
uuid_sensor=sensor_id
|
farm_uuid=sensor_id
|
||||||
)
|
)
|
||||||
except SensorData.DoesNotExist:
|
except SensorData.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
location = sensor.location
|
location = sensor.center_location
|
||||||
depths = list(
|
depths = list(
|
||||||
SoilDepthData.objects.filter(soil_location=location).order_by("depth_label")
|
SoilDepthData.objects.filter(soil_location=location).order_by("depth_label")
|
||||||
)
|
)
|
||||||
@@ -22,9 +22,6 @@ def load_dashboard_context(sensor_id: str) -> dict | None:
|
|||||||
WeatherForecast.objects.filter(location=location, forecast_date__gte=date.today())
|
WeatherForecast.objects.filter(location=location, forecast_date__gte=date.today())
|
||||||
.order_by("forecast_date")[:7]
|
.order_by("forecast_date")[:7]
|
||||||
)
|
)
|
||||||
history = list(
|
|
||||||
SensorDataHistory.objects.filter(uuid_sensor=sensor_id).order_by("-recorded_at")[:30]
|
|
||||||
)
|
|
||||||
plants = list(sensor.plants.all())
|
plants = list(sensor.plants.all())
|
||||||
irrigation_methods = list(IrrigationMethod.objects.all()[:5])
|
irrigation_methods = list(IrrigationMethod.objects.all()[:5])
|
||||||
|
|
||||||
@@ -33,8 +30,7 @@ def load_dashboard_context(sensor_id: str) -> dict | None:
|
|||||||
"location": location,
|
"location": location,
|
||||||
"depths": depths,
|
"depths": depths,
|
||||||
"forecasts": forecasts,
|
"forecasts": forecasts,
|
||||||
"history": history,
|
"history": [],
|
||||||
"plants": plants,
|
"plants": plants,
|
||||||
"irrigation_methods": irrigation_methods,
|
"irrigation_methods": irrigation_methods,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def _farm_profile_from_context(context: dict) -> dict:
|
|||||||
irrigation_methods = context.get("irrigation_methods", [])
|
irrigation_methods = context.get("irrigation_methods", [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"sensor_id": str(getattr(sensor, "uuid_sensor", "")) if sensor else "",
|
"sensor_id": str(getattr(sensor, "farm_uuid", "")) if sensor else "",
|
||||||
"crop_type": getattr(plants[0], "name", None) if plants else None,
|
"crop_type": getattr(plants[0], "name", None) if plants else None,
|
||||||
"region": {
|
"region": {
|
||||||
"latitude": float(location.latitude) if location else None,
|
"latitude": float(location.latitude) if location else None,
|
||||||
|
|||||||
@@ -1,10 +1,47 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
wait_for_db() {
|
||||||
|
python - <<'PY'
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
host = os.environ.get("DB_HOST", "db")
|
||||||
|
port = int(os.environ.get("DB_PORT", "3306"))
|
||||||
|
deadline = time.time() + 90
|
||||||
|
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
with socket.create_connection((host, port), timeout=3):
|
||||||
|
print(f"Database is reachable at {host}:{port}")
|
||||||
|
sys.exit(0)
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"Waiting for database {host}:{port}... {exc}")
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
print(f"Timed out waiting for database {host}:{port}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
if [ "${SKIP_MIGRATE}" != "1" ]; then
|
if [ "${SKIP_MIGRATE}" != "1" ]; then
|
||||||
|
wait_for_db
|
||||||
echo "Running migrations..."
|
echo "Running migrations..."
|
||||||
python manage.py repair_location_tables
|
python manage.py repair_location_tables
|
||||||
python manage.py migrate --noinput
|
python manage.py migrate --noinput
|
||||||
echo "Migrations done."
|
echo "Migrations done."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "${DEVELOP}" ] && [ "${SKIP_MIGRATE}" != "1" ]; then
|
||||||
|
echo "DEVELOP is set. Seeding demo plant, location_data, weather_data, and farm_data..."
|
||||||
|
python manage.py seed_plants
|
||||||
|
python manage.py seed_location_data
|
||||||
|
python manage.py seed_weather_data
|
||||||
|
python manage.py seed_farm_data
|
||||||
|
echo "Demo seeders done."
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Starting command: $*"
|
echo "Starting command: $*"
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import ParameterUpdateLog, SensorData, SensorParameter
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SensorData)
|
||||||
|
class SensorDataAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"farm_uuid",
|
||||||
|
"center_location_id",
|
||||||
|
"weather_forecast_id",
|
||||||
|
"sensor_keys",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
|
list_filter = ("updated_at",)
|
||||||
|
search_fields = ("farm_uuid", "center_location_id")
|
||||||
|
filter_horizontal = ("plants",)
|
||||||
|
|
||||||
|
@admin.display(description="sensor keys")
|
||||||
|
def sensor_keys(self, obj):
|
||||||
|
payload = obj.sensor_payload if isinstance(obj.sensor_payload, dict) else {}
|
||||||
|
return ", ".join(payload.keys())
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SensorParameter)
|
||||||
|
class SensorParameterAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("sensor_key", "code", "name_fa", "unit", "data_type", "created_at")
|
||||||
|
search_fields = ("sensor_key", "code", "name_fa")
|
||||||
|
list_filter = ("sensor_key", "data_type")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ParameterUpdateLog)
|
||||||
|
class ParameterUpdateLogAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("parameter", "action", "updated_at")
|
||||||
|
list_filter = ("action", "updated_at")
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FarmDataConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "farm_data"
|
||||||
|
label = "sensor_data"
|
||||||
|
verbose_name = "farm-data"
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
Management command to seed a fixed demo farm-data record.
|
||||||
|
Run: python manage.py seed_farm_data
|
||||||
|
"""
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from farm_data.models import SensorData
|
||||||
|
from location_data.models import SoilLocation
|
||||||
|
from plant.models import Plant
|
||||||
|
from weather.models import WeatherForecast
|
||||||
|
|
||||||
|
|
||||||
|
DEMO_FARM_UUID = UUID("11111111-1111-1111-1111-111111111111")
|
||||||
|
DEMO_LATITUDE = "50.000000"
|
||||||
|
DEMO_LONGITUDE = "50.000000"
|
||||||
|
DEMO_SENSOR_PAYLOAD = {
|
||||||
|
"sensor-7-1": {
|
||||||
|
"soil_moisture": 42.3,
|
||||||
|
"soil_temperature": 21.4,
|
||||||
|
"soil_ph": 6.9,
|
||||||
|
"electrical_conductivity": 1.1,
|
||||||
|
"nitrogen": 28.0,
|
||||||
|
"phosphorus": 14.0,
|
||||||
|
"potassium": 19.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DEMO_PLANT_NAMES = [
|
||||||
|
"گوجهفرنگی",
|
||||||
|
"خیار",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Seed a fixed farm-data row with farm_uuid=11111111-1111-1111-1111-111111111111."
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
location, _ = SoilLocation.objects.get_or_create(
|
||||||
|
latitude=DEMO_LATITUDE,
|
||||||
|
longitude=DEMO_LONGITUDE,
|
||||||
|
)
|
||||||
|
weather_forecast = (
|
||||||
|
WeatherForecast.objects.filter(location=location)
|
||||||
|
.order_by("-forecast_date", "-id")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
farm_data, created = SensorData.objects.update_or_create(
|
||||||
|
farm_uuid=DEMO_FARM_UUID,
|
||||||
|
defaults={
|
||||||
|
"center_location": location,
|
||||||
|
"weather_forecast": weather_forecast,
|
||||||
|
"sensor_payload": DEMO_SENSOR_PAYLOAD,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
plants = list(Plant.objects.filter(name__in=DEMO_PLANT_NAMES).order_by("name"))
|
||||||
|
if plants:
|
||||||
|
farm_data.plants.set(plants)
|
||||||
|
|
||||||
|
status_text = "Created" if created else "Updated"
|
||||||
|
weather_text = weather_forecast.id if weather_forecast else "None"
|
||||||
|
plant_count = len(plants)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"{status_text} farm-data {farm_data.farm_uuid} for center_location_id={location.id} weather_forecast_id={weather_text} plants={plant_count}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS("\nDone seeding farm_data demo record."))
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
Management command to seed the 7 initial sensor parameters.
|
||||||
|
Run: python manage.py seed_sensor_parameters
|
||||||
|
"""
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from farm_data.models import (
|
||||||
|
DEFAULT_SENSOR_DATA_TYPE,
|
||||||
|
DEFAULT_SENSOR_KEY,
|
||||||
|
ParameterUpdateLog,
|
||||||
|
SensorParameter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
INITIAL_PARAMETERS = [
|
||||||
|
("soil_moisture", "رطوبت خاک", "%"),
|
||||||
|
("soil_temperature", "دما خاک", "°C"),
|
||||||
|
("soil_ph", "pH خاک", ""),
|
||||||
|
("electrical_conductivity", "هدایت الکتریکی", "dS/m"),
|
||||||
|
("nitrogen", "ازت (N)", "mg/kg"),
|
||||||
|
("phosphorus", "فسفر", "mg/kg"),
|
||||||
|
("potassium", "پتاسیم", "mg/kg"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Seed 7 initial sensor parameters (soil_moisture, soil_temperature, etc.)"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--sensor-key",
|
||||||
|
default=DEFAULT_SENSOR_KEY,
|
||||||
|
help='کلید سنسور مثل "sensor-7-1" یا "leaf-sensor"',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
sensor_key = options["sensor_key"]
|
||||||
|
created_count = 0
|
||||||
|
for code, name_fa, unit in INITIAL_PARAMETERS:
|
||||||
|
param, created = SensorParameter.objects.get_or_create(
|
||||||
|
sensor_key=sensor_key,
|
||||||
|
code=code,
|
||||||
|
defaults={
|
||||||
|
"name_fa": name_fa,
|
||||||
|
"unit": unit,
|
||||||
|
"data_type": DEFAULT_SENSOR_DATA_TYPE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
ParameterUpdateLog.objects.create(
|
||||||
|
parameter=param,
|
||||||
|
action="added",
|
||||||
|
payload={
|
||||||
|
"sensor_key": sensor_key,
|
||||||
|
"code": code,
|
||||||
|
"name_fa": name_fa,
|
||||||
|
"unit": unit,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
created_count += 1
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f" Created: {sensor_key}.{code} ({name_fa})"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.SUCCESS(
|
||||||
|
f"\nDone. Created {created_count} new parameters for {sensor_key}."
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("sensor_data", "0004_alter_sensordata_location"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="SensorDataHistory",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SENSOR_KEY = "sensor-7-1"
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_sensor_fields_to_payload(apps, schema_editor):
|
||||||
|
SensorData = apps.get_model("sensor_data", "SensorData")
|
||||||
|
field_names = [
|
||||||
|
"soil_moisture",
|
||||||
|
"soil_temperature",
|
||||||
|
"soil_ph",
|
||||||
|
"electrical_conductivity",
|
||||||
|
"nitrogen",
|
||||||
|
"phosphorus",
|
||||||
|
"potassium",
|
||||||
|
]
|
||||||
|
|
||||||
|
for sensor in SensorData.objects.all().iterator():
|
||||||
|
values = {}
|
||||||
|
for field_name in field_names:
|
||||||
|
value = getattr(sensor, field_name, None)
|
||||||
|
if value is not None:
|
||||||
|
values[field_name] = value
|
||||||
|
|
||||||
|
sensor.sensor_payload = {DEFAULT_SENSOR_KEY: values} if values else {}
|
||||||
|
sensor.save(update_fields=["sensor_payload"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("sensor_data", "0005_delete_sensordatahistory"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="sensor_payload",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
default=dict,
|
||||||
|
help_text='اطلاعات سنسورها در فرمت {"sensor-7-1": {...}}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="sensorparameter",
|
||||||
|
name="sensor_key",
|
||||||
|
field=models.CharField(
|
||||||
|
db_index=True,
|
||||||
|
default=DEFAULT_SENSOR_KEY,
|
||||||
|
help_text='کلید سنسور داخل JSON مثل "sensor-7-1" یا "leaf-sensor"',
|
||||||
|
max_length=64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="sensorparameter",
|
||||||
|
name="data_type",
|
||||||
|
field=models.CharField(
|
||||||
|
default="float",
|
||||||
|
help_text="نوع داده پارامتر مثل float, int, string, bool",
|
||||||
|
max_length=32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="sensorparameter",
|
||||||
|
name="metadata",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
default=dict,
|
||||||
|
help_text="اطلاعات تکمیلی پارامتر مثل بازه مجاز، توضیح یا تنظیمات UI",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="parameterupdatelog",
|
||||||
|
name="payload",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
default=dict,
|
||||||
|
help_text="خلاصه تغییرات پارامتر برای audit",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_sensor_fields_to_payload,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="soil_moisture",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="soil_temperature",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="soil_ph",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="electrical_conductivity",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="nitrogen",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="phosphorus",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="potassium",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="sensorparameter",
|
||||||
|
name="code",
|
||||||
|
field=models.CharField(
|
||||||
|
db_index=True,
|
||||||
|
help_text="کد پارامتر (مثلاً soil_moisture)",
|
||||||
|
max_length=64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="sensorparameter",
|
||||||
|
options={
|
||||||
|
"ordering": ["sensor_key", "code"],
|
||||||
|
"verbose_name": "پارامتر سنسور",
|
||||||
|
"verbose_name_plural": "پارامترهای سنسور",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="sensorparameter",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("sensor_key", "code"),
|
||||||
|
name="sensor_parameter_unique_sensor_code",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("sensor_data", "0006_sensor_payload_and_dynamic_parameters"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="sensordata",
|
||||||
|
old_name="uuid_sensor",
|
||||||
|
new_name="farm_uuid",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("location_data", "0005_merge_20260327_0840"),
|
||||||
|
("sensor_data", "0007_rename_uuid_sensor_to_farm_uuid"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="sensordata",
|
||||||
|
old_name="location",
|
||||||
|
new_name="center_location",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="center_location",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
db_column="center_location_id",
|
||||||
|
help_text="مرکز زمین مرتبط از جدول location_data.SoilLocation",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="farm_data",
|
||||||
|
to="location_data.soillocation",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def link_latest_weather_forecast(apps, schema_editor):
|
||||||
|
SensorData = apps.get_model("sensor_data", "SensorData")
|
||||||
|
WeatherForecast = apps.get_model("weather", "WeatherForecast")
|
||||||
|
|
||||||
|
for farm_data in SensorData.objects.all().iterator():
|
||||||
|
forecast = (
|
||||||
|
WeatherForecast.objects.filter(location_id=farm_data.center_location_id)
|
||||||
|
.order_by("-forecast_date", "-id")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if forecast:
|
||||||
|
farm_data.weather_forecast_id = forecast.id
|
||||||
|
farm_data.save(update_fields=["weather_forecast"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("sensor_data", "0008_rename_location_to_center_location"),
|
||||||
|
("weather", "0003_seed_weather_forecasts"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="weather_forecast",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
db_column="weather_forecast_id",
|
||||||
|
help_text="رکورد آب وهوای مرتبط با مرکز زمین",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="farm_data_entries",
|
||||||
|
to="weather.weatherforecast",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
link_latest_weather_forecast,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("sensor_data", "0009_add_weather_forecast_to_sensordata"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="farm_uuid",
|
||||||
|
field=models.UUIDField(
|
||||||
|
editable=False,
|
||||||
|
help_text="شناسه یکتای farm که از API دریافت میشود",
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="sensordata",
|
||||||
|
name="plants",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
db_table="farm_data_sensordata_plants",
|
||||||
|
help_text="گیاهان مرتبط با این farm",
|
||||||
|
related_name="farm_data",
|
||||||
|
to="plant.plant",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name="sensordata",
|
||||||
|
table="farm_data_sensordata",
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name="sensorparameter",
|
||||||
|
table="farm_data_sensorparameter",
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name="parameterupdatelog",
|
||||||
|
table="farm_data_parameterupdatelog",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SENSOR_KEY = "sensor-7-1"
|
||||||
|
DEFAULT_SENSOR_DATA_TYPE = "float"
|
||||||
|
|
||||||
|
|
||||||
|
class SensorPayloadMixin:
|
||||||
|
"""دسترسی سازگار به مقادیر سنسور از payload پویا."""
|
||||||
|
|
||||||
|
sensor_payload: dict
|
||||||
|
|
||||||
|
def _payload(self) -> dict:
|
||||||
|
if isinstance(self.sensor_payload, dict):
|
||||||
|
return self.sensor_payload
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_sensor_block(self, sensor_key: str | None = None) -> dict:
|
||||||
|
payload = self._payload()
|
||||||
|
if sensor_key:
|
||||||
|
block = payload.get(sensor_key, {})
|
||||||
|
return block if isinstance(block, dict) else {}
|
||||||
|
|
||||||
|
for block in payload.values():
|
||||||
|
if isinstance(block, dict):
|
||||||
|
return block
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_metric(self, metric_name: str, sensor_key: str | None = None):
|
||||||
|
block = self.get_sensor_block(sensor_key)
|
||||||
|
if metric_name in block:
|
||||||
|
return block.get(metric_name)
|
||||||
|
|
||||||
|
for candidate in self._payload().values():
|
||||||
|
if isinstance(candidate, dict) and metric_name in candidate:
|
||||||
|
return candidate.get(metric_name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def soil_moisture(self):
|
||||||
|
return self.get_metric("soil_moisture")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def soil_temperature(self):
|
||||||
|
return self.get_metric("soil_temperature")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def soil_ph(self):
|
||||||
|
return self.get_metric("soil_ph")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def electrical_conductivity(self):
|
||||||
|
return self.get_metric("electrical_conductivity")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nitrogen(self):
|
||||||
|
return self.get_metric("nitrogen")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def phosphorus(self):
|
||||||
|
return self.get_metric("phosphorus")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def potassium(self):
|
||||||
|
return self.get_metric("potassium")
|
||||||
|
|
||||||
|
|
||||||
|
class SensorData(SensorPayloadMixin, models.Model):
|
||||||
|
"""
|
||||||
|
دادههای مزرعه/سنسور برای مرکز زمین.
|
||||||
|
مقادیر سنسورها بهصورت JSON ذخیره میشوند تا بتوان چند نوع سنسور
|
||||||
|
و پارامترهای دلخواه را در یک رکورد نگه داشت.
|
||||||
|
نمونه:
|
||||||
|
{
|
||||||
|
"sensor-7-1": {
|
||||||
|
"soil_moisture": 22.4,
|
||||||
|
"soil_temperature": 18.1
|
||||||
|
},
|
||||||
|
"leaf-sensor": {
|
||||||
|
"leaf_wetness": 11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
farm_uuid = models.UUIDField(
|
||||||
|
primary_key=True,
|
||||||
|
editable=False,
|
||||||
|
help_text="شناسه یکتای farm که از API دریافت میشود",
|
||||||
|
)
|
||||||
|
center_location = models.ForeignKey(
|
||||||
|
"location_data.SoilLocation",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="farm_data",
|
||||||
|
db_column="center_location_id",
|
||||||
|
help_text="مرکز زمین مرتبط از جدول location_data.SoilLocation",
|
||||||
|
)
|
||||||
|
weather_forecast = models.ForeignKey(
|
||||||
|
"weather.WeatherForecast",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="farm_data_entries",
|
||||||
|
db_column="weather_forecast_id",
|
||||||
|
help_text="رکورد آب وهوای مرتبط با مرکز زمین",
|
||||||
|
)
|
||||||
|
sensor_payload = models.JSONField(
|
||||||
|
default=dict,
|
||||||
|
blank=True,
|
||||||
|
help_text='اطلاعات سنسورها در فرمت {"sensor-7-1": {...}}',
|
||||||
|
)
|
||||||
|
plants = models.ManyToManyField(
|
||||||
|
"plant.Plant",
|
||||||
|
blank=True,
|
||||||
|
db_table="farm_data_sensordata_plants",
|
||||||
|
related_name="farm_data",
|
||||||
|
help_text="گیاهان مرتبط با این farm",
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "farm_data_sensordata"
|
||||||
|
ordering = ["-updated_at"]
|
||||||
|
verbose_name = "farm-data"
|
||||||
|
verbose_name_plural = "farm-data"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (
|
||||||
|
f"SensorData({self.farm_uuid}, center_location={self.center_location_id}, "
|
||||||
|
f"weather_forecast={self.weather_forecast_id})"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location(self):
|
||||||
|
return self.center_location
|
||||||
|
|
||||||
|
@location.setter
|
||||||
|
def location(self, value):
|
||||||
|
self.center_location = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_id(self):
|
||||||
|
return self.center_location_id
|
||||||
|
|
||||||
|
|
||||||
|
class SensorParameter(models.Model):
|
||||||
|
"""
|
||||||
|
تعریف پارامترهای سنسور برای هر نوع سنسور.
|
||||||
|
با این ساختار میتوان برای sensor-7-1 یا هر سنسور جدید،
|
||||||
|
پارامترهای اختصاصی تعریف کرد.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sensor_key = models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
db_index=True,
|
||||||
|
default=DEFAULT_SENSOR_KEY,
|
||||||
|
help_text='کلید سنسور داخل JSON مثل "sensor-7-1" یا "leaf-sensor"',
|
||||||
|
)
|
||||||
|
code = models.CharField(
|
||||||
|
max_length=64,
|
||||||
|
db_index=True,
|
||||||
|
help_text="کد پارامتر (مثلاً soil_moisture)",
|
||||||
|
)
|
||||||
|
name_fa = models.CharField(max_length=128, help_text="نام فارسی")
|
||||||
|
unit = models.CharField(max_length=32, blank=True, help_text="واحد اندازهگیری")
|
||||||
|
data_type = models.CharField(
|
||||||
|
max_length=32,
|
||||||
|
default=DEFAULT_SENSOR_DATA_TYPE,
|
||||||
|
help_text="نوع داده پارامتر مثل float, int, string, bool",
|
||||||
|
)
|
||||||
|
metadata = models.JSONField(
|
||||||
|
default=dict,
|
||||||
|
blank=True,
|
||||||
|
help_text="اطلاعات تکمیلی پارامتر مثل بازه مجاز، توضیح یا تنظیمات UI",
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "farm_data_sensorparameter"
|
||||||
|
ordering = ["sensor_key", "code"]
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["sensor_key", "code"],
|
||||||
|
name="sensor_parameter_unique_sensor_code",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
verbose_name = "پارامتر سنسور"
|
||||||
|
verbose_name_plural = "پارامترهای سنسور"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.sensor_key}.{self.code} ({self.name_fa})"
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterUpdateLog(models.Model):
|
||||||
|
"""
|
||||||
|
لاگ آپدیت لیست پارامترها.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ACTION_ADDED = "added"
|
||||||
|
ACTION_MODIFIED = "modified"
|
||||||
|
ACTION_CHOICES = [
|
||||||
|
(ACTION_ADDED, "اضافه شده"),
|
||||||
|
(ACTION_MODIFIED, "ویرایش شده"),
|
||||||
|
]
|
||||||
|
|
||||||
|
parameter = models.ForeignKey(
|
||||||
|
SensorParameter,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="update_logs",
|
||||||
|
)
|
||||||
|
action = models.CharField(max_length=16, choices=ACTION_CHOICES)
|
||||||
|
payload = models.JSONField(
|
||||||
|
default=dict,
|
||||||
|
blank=True,
|
||||||
|
help_text="خلاصه تغییرات پارامتر برای audit",
|
||||||
|
)
|
||||||
|
updated_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "farm_data_parameterupdatelog"
|
||||||
|
ordering = ["-updated_at"]
|
||||||
|
verbose_name = "لاگ آپدیت پارامتر"
|
||||||
|
verbose_name_plural = "لاگ آپدیت پارامترها"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.parameter.code} - {self.action} - {self.updated_at}"
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"name": "Farm Data",
|
||||||
|
"description": "API دادههای farm: ایجاد/آپدیت رکورد farm و مدیریت پارامترهای سنسور",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
|
},
|
||||||
|
"variable": [
|
||||||
|
{"key": "baseUrl", "value": "http://localhost:8020"},
|
||||||
|
{"key": "farm_uuid", "value": "00000000-0000-0000-0000-000000000000"}
|
||||||
|
],
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Upsert Farm Data (POST)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{"key": "Content-Type", "value": "application/json"},
|
||||||
|
{"key": "Accept", "value": "application/json"}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"farm_uuid\": \"{{farm_uuid}}\",\n \"farm_boundary\": {\n \"corners\": [\n {\"lat\": 35.7000, \"lon\": 51.3900},\n {\"lat\": 35.7000, \"lon\": 51.4100},\n {\"lat\": 35.7200, \"lon\": 51.4100},\n {\"lat\": 35.7200, \"lon\": 51.3900}\n ]\n },\n \"sensor_payload\": {\n \"sensor-7-1\": {\n \"soil_moisture\": 25.5,\n \"soil_temperature\": 22.3,\n \"soil_ph\": 7.2,\n \"electrical_conductivity\": 1.8,\n \"nitrogen\": 120.0,\n \"phosphorus\": 45.0,\n \"potassium\": 180.0\n }\n }\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/farm-data/",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["api", "farm-data", ""]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "ایجاد یا آپدیت داده farm. مختصات گوشههای زمین را میگیرد، مرکز را خودش محاسبه میکند، location را میسازد و weather را از همان location پیدا میکند."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Add Parameter",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{"key": "Content-Type", "value": "application/json"},
|
||||||
|
{"key": "Accept", "value": "application/json"}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"sensor_key\": \"sensor-7-1\",\n \"code\": \"soil_moisture\",\n \"name_fa\": \"رطوبت خاک\",\n \"unit\": \"%\",\n \"data_type\": \"float\",\n \"metadata\": {\n \"min\": 0,\n \"max\": 100\n }\n}"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseUrl}}/api/farm-data/parameters/",
|
||||||
|
"host": ["{{baseUrl}}"],
|
||||||
|
"path": ["api", "farm-data", "parameters", ""]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "اضافه کردن یا ویرایش پارامتر جدید. در ParameterUpdateLog ثبت میشود."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from location_data.serializers import SoilDepthDataSerializer
|
||||||
|
from plant.serializers import PlantSerializer
|
||||||
|
from weather.models import WeatherForecast
|
||||||
|
|
||||||
|
from .models import DEFAULT_SENSOR_DATA_TYPE, DEFAULT_SENSOR_KEY, SensorData
|
||||||
|
|
||||||
|
|
||||||
|
class SensorDataUpdateSerializer(serializers.Serializer):
|
||||||
|
"""ورودی آپدیت داده سنسور در ساختار JSON."""
|
||||||
|
|
||||||
|
farm_uuid = serializers.UUIDField(required=True)
|
||||||
|
farm_boundary = serializers.JSONField(required=True)
|
||||||
|
sensor_key = serializers.CharField(required=False, default=DEFAULT_SENSOR_KEY)
|
||||||
|
sensor_payload = serializers.JSONField(required=False)
|
||||||
|
plant_ids = serializers.ListField(
|
||||||
|
child=serializers.IntegerField(),
|
||||||
|
required=False,
|
||||||
|
help_text="لیست شناسه گیاهان مرتبط",
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise serializers.ValidationError("بدنه درخواست باید JSON object باشد.")
|
||||||
|
|
||||||
|
payload = dict(data)
|
||||||
|
known_fields = {
|
||||||
|
"farm_uuid",
|
||||||
|
"farm_boundary",
|
||||||
|
"sensor_key",
|
||||||
|
"sensor_payload",
|
||||||
|
"plant_ids",
|
||||||
|
}
|
||||||
|
flat_metrics = {
|
||||||
|
key: value
|
||||||
|
for key, value in payload.items()
|
||||||
|
if key not in known_fields
|
||||||
|
}
|
||||||
|
|
||||||
|
if flat_metrics:
|
||||||
|
sensor_key = payload.get("sensor_key", DEFAULT_SENSOR_KEY)
|
||||||
|
nested_payload = payload.get("sensor_payload") or {}
|
||||||
|
if nested_payload and not isinstance(nested_payload, dict):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"sensor_payload": "sensor_payload باید object باشد."}
|
||||||
|
)
|
||||||
|
merged_payload = dict(nested_payload)
|
||||||
|
current_sensor_payload = merged_payload.get(sensor_key, {})
|
||||||
|
if current_sensor_payload and not isinstance(current_sensor_payload, dict):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
{"sensor_payload": f"مقدار {sensor_key} باید object باشد."}
|
||||||
|
)
|
||||||
|
merged_sensor_payload = dict(current_sensor_payload)
|
||||||
|
merged_sensor_payload.update(flat_metrics)
|
||||||
|
merged_payload[sensor_key] = merged_sensor_payload
|
||||||
|
payload["sensor_payload"] = merged_payload
|
||||||
|
|
||||||
|
for key in flat_metrics:
|
||||||
|
payload.pop(key, None)
|
||||||
|
|
||||||
|
return super().to_internal_value(payload)
|
||||||
|
|
||||||
|
def validate_sensor_payload(self, value):
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise serializers.ValidationError("sensor_payload باید object باشد.")
|
||||||
|
for sensor_key, sensor_values in value.items():
|
||||||
|
if not isinstance(sensor_values, dict):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
f"مقدار سنسور {sensor_key} باید object باشد."
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
if "sensor_payload" not in attrs and "plant_ids" not in attrs:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"حداقل یکی از sensor_payload یا plant_ids باید ارسال شود."
|
||||||
|
)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class SensorDataResponseSerializer(serializers.ModelSerializer):
|
||||||
|
"""سریالایزر خروجی برای SensorData."""
|
||||||
|
|
||||||
|
plant_ids = serializers.PrimaryKeyRelatedField(
|
||||||
|
source="plants",
|
||||||
|
many=True,
|
||||||
|
read_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SensorData
|
||||||
|
fields = [
|
||||||
|
"farm_uuid",
|
||||||
|
"center_location_id",
|
||||||
|
"weather_forecast_id",
|
||||||
|
"sensor_payload",
|
||||||
|
"plant_ids",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SensorParameterSerializer(serializers.Serializer):
|
||||||
|
"""سریالایزر ورودی برای تعریف پارامترهای سنسورهای مختلف."""
|
||||||
|
|
||||||
|
sensor_key = serializers.CharField(max_length=64, required=False, default=DEFAULT_SENSOR_KEY)
|
||||||
|
code = serializers.CharField(max_length=64)
|
||||||
|
name_fa = serializers.CharField(max_length=128)
|
||||||
|
unit = serializers.CharField(max_length=32, required=False, allow_blank=True)
|
||||||
|
data_type = serializers.CharField(
|
||||||
|
max_length=32,
|
||||||
|
required=False,
|
||||||
|
default=DEFAULT_SENSOR_DATA_TYPE,
|
||||||
|
)
|
||||||
|
metadata = serializers.JSONField(required=False, default=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class FarmCenterLocationSerializer(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField()
|
||||||
|
lat = serializers.DecimalField(max_digits=9, decimal_places=6)
|
||||||
|
lon = serializers.DecimalField(max_digits=9, decimal_places=6)
|
||||||
|
farm_boundary = serializers.JSONField()
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherForecastDetailSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = WeatherForecast
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"forecast_date",
|
||||||
|
"temperature_min",
|
||||||
|
"temperature_max",
|
||||||
|
"temperature_mean",
|
||||||
|
"precipitation",
|
||||||
|
"precipitation_probability",
|
||||||
|
"humidity_mean",
|
||||||
|
"wind_speed_max",
|
||||||
|
"et0",
|
||||||
|
"weather_code",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FarmSoilPayloadSerializer(serializers.Serializer):
|
||||||
|
resolved_metrics = serializers.JSONField()
|
||||||
|
metric_sources = serializers.JSONField()
|
||||||
|
depths = SoilDepthDataSerializer(many=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FarmDetailSerializer(serializers.Serializer):
|
||||||
|
farm_uuid = serializers.UUIDField()
|
||||||
|
center_location = FarmCenterLocationSerializer()
|
||||||
|
weather = WeatherForecastDetailSerializer(allow_null=True)
|
||||||
|
sensor_payload = serializers.JSONField()
|
||||||
|
soil = FarmSoilPayloadSerializer()
|
||||||
|
plant_ids = serializers.ListField(child=serializers.IntegerField())
|
||||||
|
plants = PlantSerializer(many=True)
|
||||||
|
created_at = serializers.DateTimeField()
|
||||||
|
updated_at = serializers.DateTimeField()
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decimal import Decimal, ROUND_HALF_UP
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from location_data.models import SoilLocation
|
||||||
|
from location_data.serializers import SoilDepthDataSerializer
|
||||||
|
from plant.serializers import PlantSerializer
|
||||||
|
from weather.models import WeatherForecast
|
||||||
|
|
||||||
|
from .models import SensorData
|
||||||
|
from .serializers import WeatherForecastDetailSerializer
|
||||||
|
|
||||||
|
|
||||||
|
DEPTH_PRIORITY = ["0-5cm", "5-15cm", "15-30cm"]
|
||||||
|
DECIMAL_PRECISION = Decimal("0.000001")
|
||||||
|
|
||||||
|
|
||||||
|
def get_farm_details(farm_uuid: str):
|
||||||
|
farm = (
|
||||||
|
SensorData.objects.select_related("center_location", "weather_forecast")
|
||||||
|
.prefetch_related("plants", "center_location__depths")
|
||||||
|
.filter(farm_uuid=farm_uuid)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if farm is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
center_location = farm.center_location
|
||||||
|
weather = farm.weather_forecast
|
||||||
|
if weather is None:
|
||||||
|
weather = (
|
||||||
|
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)
|
||||||
|
sensor_metrics = _flatten_sensor_metrics(farm.sensor_payload)
|
||||||
|
|
||||||
|
resolved_metrics = dict(soil_metrics)
|
||||||
|
metric_sources = {key: "soil" for key in soil_metrics}
|
||||||
|
for key, value in sensor_metrics.items():
|
||||||
|
resolved_metrics[key] = value
|
||||||
|
metric_sources[key] = "sensor"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"farm_uuid": farm.farm_uuid,
|
||||||
|
"center_location": {
|
||||||
|
"id": center_location.id,
|
||||||
|
"lat": center_location.latitude,
|
||||||
|
"lon": center_location.longitude,
|
||||||
|
"farm_boundary": center_location.farm_boundary,
|
||||||
|
},
|
||||||
|
"weather": WeatherForecastDetailSerializer(weather).data if weather else None,
|
||||||
|
"sensor_payload": farm.sensor_payload or {},
|
||||||
|
"soil": {
|
||||||
|
"resolved_metrics": resolved_metrics,
|
||||||
|
"metric_sources": metric_sources,
|
||||||
|
"depths": SoilDepthDataSerializer(depths, many=True).data,
|
||||||
|
},
|
||||||
|
"plant_ids": list(farm.plants.values_list("id", flat=True)),
|
||||||
|
"plants": PlantSerializer(farm.plants.all(), many=True).data,
|
||||||
|
"created_at": farm.created_at,
|
||||||
|
"updated_at": farm.updated_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_center_location_from_boundary(farm_boundary: dict | list) -> SoilLocation:
|
||||||
|
"""
|
||||||
|
مرز مزرعه را میگیرد، مرکز را محاسبه میکند و رکورد SoilLocation را
|
||||||
|
ایجاد/بهروزرسانی میکند.
|
||||||
|
"""
|
||||||
|
points = _extract_boundary_points(farm_boundary)
|
||||||
|
if not points:
|
||||||
|
raise ValueError("farm_boundary باید حداقل 3 گوشه معتبر داشته باشد.")
|
||||||
|
|
||||||
|
normalized_points = _normalize_points(points)
|
||||||
|
if len(normalized_points) < 3:
|
||||||
|
raise ValueError("farm_boundary باید حداقل 3 گوشه معتبر داشته باشد.")
|
||||||
|
|
||||||
|
lat_sum = sum(lat for lat, _ in normalized_points)
|
||||||
|
lon_sum = sum(lon for _, lon in normalized_points)
|
||||||
|
count = Decimal(len(normalized_points))
|
||||||
|
center_lat = (lat_sum / count).quantize(DECIMAL_PRECISION, rounding=ROUND_HALF_UP)
|
||||||
|
center_lon = (lon_sum / count).quantize(DECIMAL_PRECISION, rounding=ROUND_HALF_UP)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
location, _ = SoilLocation.objects.update_or_create(
|
||||||
|
latitude=center_lat,
|
||||||
|
longitude=center_lon,
|
||||||
|
defaults={"farm_boundary": _serialize_boundary(farm_boundary)},
|
||||||
|
)
|
||||||
|
return location
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_weather_for_location(location: SoilLocation) -> WeatherForecast | None:
|
||||||
|
return (
|
||||||
|
WeatherForecast.objects.filter(location=location)
|
||||||
|
.order_by("-forecast_date", "-id")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _flatten_sensor_metrics(sensor_payload: dict | None) -> dict:
|
||||||
|
if not isinstance(sensor_payload, dict):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
flattened = {}
|
||||||
|
for sensor_values in sensor_payload.values():
|
||||||
|
if not isinstance(sensor_values, dict):
|
||||||
|
continue
|
||||||
|
flattened.update(sensor_values)
|
||||||
|
return flattened
|
||||||
|
|
||||||
|
|
||||||
|
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":
|
||||||
|
coordinates = boundary.get("coordinates") or []
|
||||||
|
if coordinates and isinstance(coordinates[0], list):
|
||||||
|
return coordinates[0]
|
||||||
|
return []
|
||||||
|
if "corners" in boundary:
|
||||||
|
return boundary.get("corners") or []
|
||||||
|
if isinstance(boundary, list):
|
||||||
|
return boundary
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_points(points: list) -> list[tuple[Decimal, Decimal]]:
|
||||||
|
normalized: list[tuple[Decimal, Decimal]] = []
|
||||||
|
for point in points:
|
||||||
|
lat = lon = None
|
||||||
|
if isinstance(point, dict):
|
||||||
|
lat = point.get("lat", point.get("latitude"))
|
||||||
|
lon = point.get("lon", point.get("longitude"))
|
||||||
|
elif isinstance(point, (list, tuple)) and len(point) >= 2:
|
||||||
|
lon, lat = point[0], point[1]
|
||||||
|
|
||||||
|
if lat is None or lon is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lat_decimal = Decimal(str(lat))
|
||||||
|
lon_decimal = Decimal(str(lon))
|
||||||
|
normalized.append((lat_decimal, lon_decimal))
|
||||||
|
|
||||||
|
if len(normalized) > 1 and normalized[0] == normalized[-1]:
|
||||||
|
normalized = normalized[:-1]
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_boundary(boundary: dict | list) -> dict:
|
||||||
|
if isinstance(boundary, dict) and boundary.get("type") == "Polygon":
|
||||||
|
return boundary
|
||||||
|
raw_points = boundary.get("corners") if isinstance(boundary, dict) else boundary
|
||||||
|
normalized = _normalize_points(raw_points or [])
|
||||||
|
coordinates = [[float(lon), float(lat)] for lat, lon in normalized]
|
||||||
|
if coordinates and coordinates[0] != coordinates[-1]:
|
||||||
|
coordinates.append(coordinates[0])
|
||||||
|
return {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [coordinates],
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
from datetime import date
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
|
from location_data.models import SoilDepthData, SoilLocation
|
||||||
|
from farm_data.models import SensorData
|
||||||
|
from plant.models import Plant
|
||||||
|
from weather.models import WeatherForecast
|
||||||
|
|
||||||
|
|
||||||
|
def square_boundary_for_center(lat: float, lon: float, delta: float = 0.01) -> dict:
|
||||||
|
return {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[
|
||||||
|
[lon - delta, lat - delta],
|
||||||
|
[lon + delta, lat - delta],
|
||||||
|
[lon + delta, lat + delta],
|
||||||
|
[lon - delta, lat + delta],
|
||||||
|
[lon - delta, lat - delta],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FarmDetailApiTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.location = SoilLocation.objects.create(
|
||||||
|
latitude="35.700000",
|
||||||
|
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),
|
||||||
|
temperature_min=12.0,
|
||||||
|
temperature_max=23.0,
|
||||||
|
temperature_mean=18.0,
|
||||||
|
precipitation=1.2,
|
||||||
|
humidity_mean=52.0,
|
||||||
|
)
|
||||||
|
self.plant1 = Plant.objects.create(name="گوجهفرنگی")
|
||||||
|
self.plant2 = Plant.objects.create(name="خیار")
|
||||||
|
self.farm_uuid = uuid.uuid4()
|
||||||
|
self.farm = SensorData.objects.create(
|
||||||
|
farm_uuid=self.farm_uuid,
|
||||||
|
center_location=self.location,
|
||||||
|
weather_forecast=self.weather,
|
||||||
|
sensor_payload={
|
||||||
|
"sensor-7-1": {
|
||||||
|
"soil_moisture": 33.5,
|
||||||
|
"nitrogen": 99.0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.farm.plants.set([self.plant2, self.plant1])
|
||||||
|
|
||||||
|
def test_returns_farm_detail_and_prioritizes_sensor_metrics_over_soil(self):
|
||||||
|
response = self.client.get(f"/api/farm-data/{self.farm_uuid}/detail/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()["data"]
|
||||||
|
|
||||||
|
self.assertEqual(payload["farm_uuid"], str(self.farm_uuid))
|
||||||
|
self.assertEqual(payload["center_location"]["id"], self.location.id)
|
||||||
|
self.assertEqual(payload["weather"]["id"], self.weather.id)
|
||||||
|
self.assertEqual(
|
||||||
|
payload["sensor_payload"]["sensor-7-1"]["soil_moisture"],
|
||||||
|
33.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved_metrics = payload["soil"]["resolved_metrics"]
|
||||||
|
metric_sources = payload["soil"]["metric_sources"]
|
||||||
|
|
||||||
|
self.assertEqual(resolved_metrics["nitrogen"], 99.0)
|
||||||
|
self.assertEqual(metric_sources["nitrogen"], "sensor")
|
||||||
|
self.assertEqual(resolved_metrics["clay"], 22.0)
|
||||||
|
self.assertEqual(metric_sources["clay"], "soil")
|
||||||
|
self.assertEqual(len(payload["soil"]["depths"]), 2)
|
||||||
|
self.assertCountEqual(payload["plant_ids"], [self.plant1.id, self.plant2.id])
|
||||||
|
self.assertEqual(len(payload["plants"]), 2)
|
||||||
|
returned_plants = {item["id"]: item for item in payload["plants"]}
|
||||||
|
self.assertEqual(returned_plants[self.plant1.id]["name"], self.plant1.name)
|
||||||
|
self.assertEqual(returned_plants[self.plant2.id]["name"], self.plant2.name)
|
||||||
|
self.assertIn("light", returned_plants[self.plant1.id])
|
||||||
|
|
||||||
|
def test_returns_404_when_farm_is_missing(self):
|
||||||
|
response = self.client.get(f"/api/farm-data/{uuid.uuid4()}/detail/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
self.assertEqual(response.json()["msg"], "farm یافت نشد.")
|
||||||
|
|
||||||
|
|
||||||
|
class FarmDataUpsertApiTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.location = SoilLocation.objects.create(
|
||||||
|
latitude="35.710000",
|
||||||
|
longitude="51.410000",
|
||||||
|
)
|
||||||
|
self.boundary = square_boundary_for_center(35.71, 51.41)
|
||||||
|
self.weather = WeatherForecast.objects.create(
|
||||||
|
location=self.location,
|
||||||
|
forecast_date=date(2026, 4, 11),
|
||||||
|
temperature_min=11.0,
|
||||||
|
temperature_max=24.0,
|
||||||
|
temperature_mean=17.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_post_creates_farm_data_with_explicit_farm_uuid(self):
|
||||||
|
farm_uuid = uuid.uuid4()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/farm-data/",
|
||||||
|
data={
|
||||||
|
"farm_uuid": str(farm_uuid),
|
||||||
|
"farm_boundary": self.boundary,
|
||||||
|
"sensor_payload": {
|
||||||
|
"sensor-7-1": {
|
||||||
|
"soil_moisture": 31.2,
|
||||||
|
"nitrogen": 18.0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(response.json()["data"]["farm_uuid"], str(farm_uuid))
|
||||||
|
self.assertEqual(response.json()["data"]["center_location_id"], self.location.id)
|
||||||
|
self.assertEqual(response.json()["data"]["weather_forecast_id"], self.weather.id)
|
||||||
|
|
||||||
|
farm = SensorData.objects.get(farm_uuid=farm_uuid)
|
||||||
|
self.assertEqual(farm.center_location_id, self.location.id)
|
||||||
|
self.assertEqual(farm.weather_forecast_id, self.weather.id)
|
||||||
|
self.assertEqual(
|
||||||
|
farm.sensor_payload["sensor-7-1"]["soil_moisture"],
|
||||||
|
31.2,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_post_requires_farm_uuid_in_request_body(self):
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/farm-data/",
|
||||||
|
data={
|
||||||
|
"farm_boundary": self.boundary,
|
||||||
|
"sensor_payload": {"sensor-7-1": {"soil_moisture": 31.2}},
|
||||||
|
},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn("farm_uuid", response.json()["data"])
|
||||||
|
|
||||||
|
def test_post_creates_center_location_from_boundary_when_missing(self):
|
||||||
|
farm_uuid = uuid.uuid4()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/farm-data/",
|
||||||
|
data={
|
||||||
|
"farm_uuid": str(farm_uuid),
|
||||||
|
"farm_boundary": {
|
||||||
|
"corners": [
|
||||||
|
{"lat": 50.0, "lon": 50.0},
|
||||||
|
{"lat": 50.0, "lon": 50.02},
|
||||||
|
{"lat": 50.02, "lon": 50.02},
|
||||||
|
{"lat": 50.02, "lon": 50.0},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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.assertIsNotNone(farm.center_location_id)
|
||||||
|
self.assertEqual(str(farm.center_location.latitude), "50.010000")
|
||||||
|
self.assertEqual(str(farm.center_location.longitude), "50.010000")
|
||||||
|
self.assertIsNone(farm.weather_forecast_id)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from .views import FarmDetailView, FarmDataUpsertView, SensorParameterCreateView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"<uuid:farm_uuid>/detail/",
|
||||||
|
FarmDetailView.as_view(),
|
||||||
|
name="farm-detail",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"",
|
||||||
|
FarmDataUpsertView.as_view(),
|
||||||
|
name="farm-data-upsert",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"parameters/",
|
||||||
|
SensorParameterCreateView.as_view(),
|
||||||
|
name="farm-parameter-create",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
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 (
|
||||||
|
get_farm_details,
|
||||||
|
resolve_center_location_from_boundary,
|
||||||
|
resolve_weather_for_location,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
"داده ورودی نامعتبر است.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
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")
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
weather_forecast = resolve_weather_for_location(center_location)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
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
|
||||||
|
if not created:
|
||||||
|
farm_data.save(
|
||||||
|
update_fields=[
|
||||||
|
"center_location",
|
||||||
|
"weather_forecast",
|
||||||
|
"sensor_payload",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
farm_data.save()
|
||||||
|
|
||||||
|
if plant_ids is not None:
|
||||||
|
farm_data.plants.set(plant_ids)
|
||||||
|
|
||||||
|
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 را برمیگرداند. "
|
||||||
|
"برای resolved_metrics، دادههای sensor روی دادههای خاک اولویت دارند."
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 202,
|
|
||||||
"msg": "dashboard task queued",
|
|
||||||
"data": {
|
|
||||||
"task_id": "dashboard-task-123",
|
|
||||||
"status_url": "/api/dashboard-data/dashboard-task-123/status/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "پارامتر sensor_id الزامی است.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "dashboard-task-123",
|
|
||||||
"status": "FAILURE",
|
|
||||||
"error": "خطا در ساخت کارتهای داشبورد."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "dashboard-task-123",
|
|
||||||
"status": "PENDING",
|
|
||||||
"message": "تسک در صف یا یافت نشد."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "dashboard-task-123",
|
|
||||||
"status": "PROGRESS",
|
|
||||||
"progress": {
|
|
||||||
"current": 5,
|
|
||||||
"total": 15,
|
|
||||||
"card": "sensorValuesList",
|
|
||||||
"message": "processing sensorValuesList"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "dashboard-task-123",
|
|
||||||
"status": "SUCCESS",
|
|
||||||
"result": {
|
|
||||||
"sensor_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"all_cards": {
|
|
||||||
"farmOverviewKpis": {
|
|
||||||
"healthScore": 82,
|
|
||||||
"activeAlerts": 2,
|
|
||||||
"waterNeedMm": 18.4
|
|
||||||
},
|
|
||||||
"sensorValuesList": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"label": "رطوبت خاک",
|
|
||||||
"value": 45.2,
|
|
||||||
"unit": "%"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "دما خاک",
|
|
||||||
"value": 22.5,
|
|
||||||
"unit": "°C"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"recommendationsList": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"recommendation_title": "تنظیم نوبت آبیاری",
|
|
||||||
"suggested_action": "آبیاری بعدی را صبح فردا انجام دهید.",
|
|
||||||
"urgency_level": "high"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 202,
|
|
||||||
"msg": "تسک توصیه کودهی در صف قرار گرفت.",
|
|
||||||
"data": {
|
|
||||||
"task_id": "fert-task-123",
|
|
||||||
"status_url": "/api/fertilization/recommend/fert-task-123/status/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "داده نامعتبر.",
|
|
||||||
"data": {
|
|
||||||
"sensor_uuid": [
|
|
||||||
"This field is required."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "fert-task-123",
|
|
||||||
"status": "FAILURE",
|
|
||||||
"error": "خطا در دریافت توصیه کودهی."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "fert-task-123",
|
|
||||||
"status": "PENDING",
|
|
||||||
"message": "تسک در صف یا یافت نشد."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "fert-task-123",
|
|
||||||
"status": "PROGRESS",
|
|
||||||
"progress": {
|
|
||||||
"message": "در حال پردازش توصیه کودهی..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "fert-task-123",
|
|
||||||
"status": "SUCCESS",
|
|
||||||
"result": {
|
|
||||||
"plan": {
|
|
||||||
"npkRatio": "20-20-20",
|
|
||||||
"amountPerHectare": "150 kg/ha",
|
|
||||||
"applicationMethod": "کودآبیاری در دو نوبت",
|
|
||||||
"applicationInterval": "هر ۱۰ روز",
|
|
||||||
"reasoning": "نیتروژن و پتاسیم خاک در محدوده متوسط است و گیاه در فاز رویشی نیاز تغذیهای بالاتری دارد."
|
|
||||||
},
|
|
||||||
"raw_response": "{\"plan\":{\"npkRatio\":\"20-20-20\"}}",
|
|
||||||
"status": "completed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,604 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/dashboard-data/generate/",
|
|
||||||
"status_code": 202,
|
|
||||||
"description": "Dashboard data task queued",
|
|
||||||
"file": "json/mock_data/dashboard-data/generate/post_202.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/dashboard-data/generate/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Missing sensor_id",
|
|
||||||
"file": "json/mock_data/dashboard-data/generate/post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/dashboard-data/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Pending dashboard task",
|
|
||||||
"file": "json/mock_data/dashboard-data/status/get_200_pending.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/dashboard-data/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Dashboard task in progress",
|
|
||||||
"file": "json/mock_data/dashboard-data/status/get_200_progress.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/dashboard-data/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Successful dashboard task",
|
|
||||||
"file": "json/mock_data/dashboard-data/status/get_200_success.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/dashboard-data/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Failed dashboard task",
|
|
||||||
"file": "json/mock_data/dashboard-data/status/get_200_failure.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/fertilization/recommend/",
|
|
||||||
"status_code": 202,
|
|
||||||
"description": "Fertilization task queued",
|
|
||||||
"file": "json/mock_data/fertilization/recommend/post_202.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/fertilization/recommend/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Validation error",
|
|
||||||
"file": "json/mock_data/fertilization/recommend/post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/fertilization/recommend/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Fertilization status pending",
|
|
||||||
"file": "json/mock_data/fertilization/status/get_200_pending.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/fertilization/recommend/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Fertilization status progress",
|
|
||||||
"file": "json/mock_data/fertilization/status/get_200_progress.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/fertilization/recommend/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Fertilization status success",
|
|
||||||
"file": "json/mock_data/fertilization/status/get_200_success.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/fertilization/recommend/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Fertilization status failure",
|
|
||||||
"file": "json/mock_data/fertilization/status/get_200_failure.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/irrigation/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "List irrigation methods",
|
|
||||||
"file": "json/mock_data/irrigation/methods/get_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/irrigation/",
|
|
||||||
"status_code": 201,
|
|
||||||
"description": "Create irrigation method",
|
|
||||||
"file": "json/mock_data/irrigation/methods/post_201.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/irrigation/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Irrigation create validation error",
|
|
||||||
"file": "json/mock_data/irrigation/methods/post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/irrigation/recommend/",
|
|
||||||
"status_code": 202,
|
|
||||||
"description": "Irrigation recommendation task queued",
|
|
||||||
"file": "json/mock_data/irrigation/recommend/post_202.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/irrigation/recommend/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Irrigation recommendation validation error",
|
|
||||||
"file": "json/mock_data/irrigation/recommend/post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/irrigation/recommend/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Irrigation recommendation status pending",
|
|
||||||
"file": "json/mock_data/irrigation/recommend/status/get_200_pending.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/irrigation/recommend/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Irrigation recommendation status progress",
|
|
||||||
"file": "json/mock_data/irrigation/recommend/status/get_200_progress.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/irrigation/recommend/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Irrigation recommendation status success",
|
|
||||||
"file": "json/mock_data/irrigation/recommend/status/get_200_success.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/irrigation/recommend/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Irrigation recommendation status failure",
|
|
||||||
"file": "json/mock_data/irrigation/recommend/status/get_200_failure.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Irrigation method get success",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/get_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Irrigation method get not found",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/get_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Irrigation method put success",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/put_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Irrigation method put validation error",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/put_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Irrigation method put not found",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/put_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Irrigation method patch success",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/patch_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Irrigation method patch validation error",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/patch_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Irrigation method patch not found",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/patch_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Delete irrigation method",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/delete_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/irrigation/{pk}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Delete irrigation method not found",
|
|
||||||
"file": "json/mock_data/irrigation/method-detail/delete_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/soil-data/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Soil data served from database",
|
|
||||||
"file": "json/mock_data/soil-data/get_200_database.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/soil-data/",
|
|
||||||
"status_code": 202,
|
|
||||||
"description": "Soil data fetch task queued",
|
|
||||||
"file": "json/mock_data/soil-data/get_202_queued.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/soil-data/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Soil data validation error",
|
|
||||||
"file": "json/mock_data/soil-data/get_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/soil-data/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Soil data POST served from database",
|
|
||||||
"file": "json/mock_data/soil-data/post_200_database.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/soil-data/",
|
|
||||||
"status_code": 202,
|
|
||||||
"description": "Soil data POST task queued",
|
|
||||||
"file": "json/mock_data/soil-data/post_202_queued.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/soil-data/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Soil data POST validation error",
|
|
||||||
"file": "json/mock_data/soil-data/post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/soil-data/tasks/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Soil task status pending",
|
|
||||||
"file": "json/mock_data/soil-data/status/get_200_pending.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/soil-data/tasks/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Soil task status progress",
|
|
||||||
"file": "json/mock_data/soil-data/status/get_200_progress.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/soil-data/tasks/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Soil task status success",
|
|
||||||
"file": "json/mock_data/soil-data/status/get_200_success.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/soil-data/tasks/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Soil task status failure",
|
|
||||||
"file": "json/mock_data/soil-data/status/get_200_failure.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/plants/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "List plants",
|
|
||||||
"file": "json/mock_data/plant/list-get_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/plants/",
|
|
||||||
"status_code": 201,
|
|
||||||
"description": "Create plant",
|
|
||||||
"file": "json/mock_data/plant/create-post_201.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/plants/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Plant create validation error",
|
|
||||||
"file": "json/mock_data/plant/create-post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Plant detail get success",
|
|
||||||
"file": "json/mock_data/plant/detail-get_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Plant detail get not found",
|
|
||||||
"file": "json/mock_data/plant/detail-get_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Plant detail put success",
|
|
||||||
"file": "json/mock_data/plant/detail-put_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Plant detail put validation error",
|
|
||||||
"file": "json/mock_data/plant/detail-put_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Plant detail put not found",
|
|
||||||
"file": "json/mock_data/plant/detail-put_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Plant detail patch success",
|
|
||||||
"file": "json/mock_data/plant/detail-patch_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Plant detail patch validation error",
|
|
||||||
"file": "json/mock_data/plant/detail-patch_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Plant detail patch not found",
|
|
||||||
"file": "json/mock_data/plant/detail-patch_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Delete plant success",
|
|
||||||
"file": "json/mock_data/plant/detail-delete_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/plants/{pk}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Delete plant not found",
|
|
||||||
"file": "json/mock_data/plant/detail-delete_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/plants/fetch-info/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Fetch plant info success",
|
|
||||||
"file": "json/mock_data/plant/fetch-info-post_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/plants/fetch-info/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Fetch plant info missing name",
|
|
||||||
"file": "json/mock_data/plant/fetch-info-post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/plants/fetch-info/",
|
|
||||||
"status_code": 503,
|
|
||||||
"description": "Fetch plant info service unavailable",
|
|
||||||
"file": "json/mock_data/plant/fetch-info-post_503.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/rag/chat/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "RAG chat streaming response",
|
|
||||||
"file": "json/mock_data/rag/chat-post_200_stream.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/rag/chat/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Missing query",
|
|
||||||
"file": "json/mock_data/rag/chat-post_400_missing_query.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/rag/chat/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Invalid service id",
|
|
||||||
"file": "json/mock_data/rag/chat-post_400_invalid_service.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/rag/chat/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Missing user_id for service",
|
|
||||||
"file": "json/mock_data/rag/chat-post_400_missing_user.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/rag/recommend/irrigation/",
|
|
||||||
"status_code": 202,
|
|
||||||
"description": "RAG irrigation task queued",
|
|
||||||
"file": "json/mock_data/rag/irrigation/post_202.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/rag/recommend/irrigation/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "RAG irrigation validation error",
|
|
||||||
"file": "json/mock_data/rag/irrigation/post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/rag/recommend/irrigation/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "RAG irrigation status pending",
|
|
||||||
"file": "json/mock_data/rag/irrigation/status/get_200_pending.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/rag/recommend/irrigation/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "RAG irrigation status progress",
|
|
||||||
"file": "json/mock_data/rag/irrigation/status/get_200_progress.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/rag/recommend/irrigation/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "RAG irrigation status success",
|
|
||||||
"file": "json/mock_data/rag/irrigation/status/get_200_success.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/rag/recommend/irrigation/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "RAG irrigation status failure",
|
|
||||||
"file": "json/mock_data/rag/irrigation/status/get_200_failure.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/rag/recommend/fertilization/",
|
|
||||||
"status_code": 202,
|
|
||||||
"description": "RAG fertilization task queued",
|
|
||||||
"file": "json/mock_data/rag/fertilization/post_202.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/rag/recommend/fertilization/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "RAG fertilization validation error",
|
|
||||||
"file": "json/mock_data/rag/fertilization/post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/rag/recommend/fertilization/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "RAG fertilization status pending",
|
|
||||||
"file": "json/mock_data/rag/fertilization/status/get_200_pending.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/rag/recommend/fertilization/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "RAG fertilization status progress",
|
|
||||||
"file": "json/mock_data/rag/fertilization/status/get_200_progress.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/rag/recommend/fertilization/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "RAG fertilization status success",
|
|
||||||
"file": "json/mock_data/rag/fertilization/status/get_200_success.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/rag/recommend/fertilization/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "RAG fertilization status failure",
|
|
||||||
"file": "json/mock_data/rag/fertilization/status/get_200_failure.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/sensor-data/{uuid_sensor}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Sensor update put success",
|
|
||||||
"file": "json/mock_data/sensor-data/update-put_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/sensor-data/{uuid_sensor}/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Sensor update put validation error",
|
|
||||||
"file": "json/mock_data/sensor-data/update-put_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/sensor-data/{uuid_sensor}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Sensor update put location not found",
|
|
||||||
"file": "json/mock_data/sensor-data/update-put_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/sensor-data/{uuid_sensor}/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Sensor update patch success",
|
|
||||||
"file": "json/mock_data/sensor-data/update-patch_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/sensor-data/{uuid_sensor}/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Sensor update patch validation error",
|
|
||||||
"file": "json/mock_data/sensor-data/update-patch_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PATCH",
|
|
||||||
"path": "/api/sensor-data/{uuid_sensor}/",
|
|
||||||
"status_code": 404,
|
|
||||||
"description": "Sensor update patch location not found",
|
|
||||||
"file": "json/mock_data/sensor-data/update-patch_404.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/sensor-data/parameters/",
|
|
||||||
"status_code": 201,
|
|
||||||
"description": "Create sensor parameter",
|
|
||||||
"file": "json/mock_data/sensor-data/parameters-post_201.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/sensor-data/parameters/",
|
|
||||||
"status_code": 400,
|
|
||||||
"description": "Sensor parameter validation error",
|
|
||||||
"file": "json/mock_data/sensor-data/parameters-post_400.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/tasks/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Task trigger success",
|
|
||||||
"file": "json/mock_data/tasks/post_200.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/tasks/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Task status pending",
|
|
||||||
"file": "json/mock_data/tasks/status/get_200_pending.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/tasks/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Task status progress",
|
|
||||||
"file": "json/mock_data/tasks/status/get_200_progress.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/tasks/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Task status success",
|
|
||||||
"file": "json/mock_data/tasks/status/get_200_success.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/tasks/{task_id}/status/",
|
|
||||||
"status_code": 200,
|
|
||||||
"description": "Task status failure",
|
|
||||||
"file": "json/mock_data/tasks/status/get_200_failure.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "روش آبیاری با موفقیت حذف شد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"msg": "روش آبیاری یافت نشد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "آبیاری قطرهای",
|
|
||||||
"category": "موضعی",
|
|
||||||
"description": "آبیاری با دبی کم و راندمان بالا",
|
|
||||||
"water_efficiency_percent": 90.0,
|
|
||||||
"water_pressure_required": "۱-۲ اتمسفر",
|
|
||||||
"flow_rate": "۲-۸ لیتر در ساعت",
|
|
||||||
"coverage_area": "بسته به طراحی سیستم",
|
|
||||||
"soil_type": "اکثر خاکها",
|
|
||||||
"climate_suitability": "گرم و خشک",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"msg": "روش آبیاری یافت نشد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "آبیاری قطرهای",
|
|
||||||
"category": "موضعی",
|
|
||||||
"description": "آبیاری با دبی کم و راندمان بالا",
|
|
||||||
"water_efficiency_percent": 90.0,
|
|
||||||
"water_pressure_required": "۱-۲ اتمسفر",
|
|
||||||
"flow_rate": "۲-۸ لیتر در ساعت",
|
|
||||||
"coverage_area": "بسته به طراحی سیستم",
|
|
||||||
"soil_type": "اکثر خاکها",
|
|
||||||
"climate_suitability": "گرم و خشک",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "داده نامعتبر.",
|
|
||||||
"data": {
|
|
||||||
"name": [
|
|
||||||
"This field may not be blank."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"msg": "روش آبیاری یافت نشد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "آبیاری قطرهای",
|
|
||||||
"category": "موضعی",
|
|
||||||
"description": "آبیاری با دبی کم و راندمان بالا",
|
|
||||||
"water_efficiency_percent": 90.0,
|
|
||||||
"water_pressure_required": "۱-۲ اتمسفر",
|
|
||||||
"flow_rate": "۲-۸ لیتر در ساعت",
|
|
||||||
"coverage_area": "بسته به طراحی سیستم",
|
|
||||||
"soil_type": "اکثر خاکها",
|
|
||||||
"climate_suitability": "گرم و خشک",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "داده نامعتبر.",
|
|
||||||
"data": {
|
|
||||||
"name": [
|
|
||||||
"This field may not be blank."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"msg": "روش آبیاری یافت نشد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "آبیاری قطرهای",
|
|
||||||
"category": "موضعی",
|
|
||||||
"description": "آبیاری با دبی کم و راندمان بالا",
|
|
||||||
"water_efficiency_percent": 90.0,
|
|
||||||
"water_pressure_required": "۱-۲ اتمسفر",
|
|
||||||
"flow_rate": "۲-۸ لیتر در ساعت",
|
|
||||||
"coverage_area": "بسته به طراحی سیستم",
|
|
||||||
"soil_type": "اکثر خاکها",
|
|
||||||
"climate_suitability": "گرم و خشک",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 201,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "آبیاری قطرهای",
|
|
||||||
"category": "موضعی",
|
|
||||||
"description": "آبیاری با دبی کم و راندمان بالا",
|
|
||||||
"water_efficiency_percent": 90.0,
|
|
||||||
"water_pressure_required": "۱-۲ اتمسفر",
|
|
||||||
"flow_rate": "۲-۸ لیتر در ساعت",
|
|
||||||
"coverage_area": "بسته به طراحی سیستم",
|
|
||||||
"soil_type": "اکثر خاکها",
|
|
||||||
"climate_suitability": "گرم و خشک",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "داده نامعتبر.",
|
|
||||||
"data": {
|
|
||||||
"name": [
|
|
||||||
"This field is required."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 202,
|
|
||||||
"msg": "تسک توصیه آبیاری در صف قرار گرفت.",
|
|
||||||
"data": {
|
|
||||||
"task_id": "irr-task-123",
|
|
||||||
"status_url": "/api/irrigation/recommend/irr-task-123/status/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "داده نامعتبر.",
|
|
||||||
"data": {
|
|
||||||
"sensor_uuid": [
|
|
||||||
"This field is required."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "irr-task-123",
|
|
||||||
"status": "FAILURE",
|
|
||||||
"error": "خطا در دریافت توصیه آبیاری."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "irr-task-123",
|
|
||||||
"status": "PENDING",
|
|
||||||
"message": "تسک در صف یا یافت نشد."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "irr-task-123",
|
|
||||||
"status": "PROGRESS",
|
|
||||||
"progress": {
|
|
||||||
"message": "در حال پردازش توصیه آبیاری..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "irr-task-123",
|
|
||||||
"status": "SUCCESS",
|
|
||||||
"result": {
|
|
||||||
"plan": {
|
|
||||||
"frequencyPerWeek": 3,
|
|
||||||
"durationMinutes": 42,
|
|
||||||
"bestTimeOfDay": "صبح زود",
|
|
||||||
"moistureLevel": 68,
|
|
||||||
"warning": "در صورت بارش موثر، نوبت سوم این هفته را حذف کنید."
|
|
||||||
},
|
|
||||||
"raw_response": "{\"plan\":{\"frequencyPerWeek\":3,\"durationMinutes\":42}}",
|
|
||||||
"water_balance": {
|
|
||||||
"daily": [
|
|
||||||
{
|
|
||||||
"forecast_date": "2025-03-25",
|
|
||||||
"et0_mm": 4.7,
|
|
||||||
"etc_mm": 5.6,
|
|
||||||
"effective_rainfall_mm": 0.0,
|
|
||||||
"gross_irrigation_mm": 6.2,
|
|
||||||
"irrigation_timing": "06:00-08:00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"crop_profile": {
|
|
||||||
"kc_initial": 0.6,
|
|
||||||
"kc_mid": 1.15,
|
|
||||||
"kc_end": 0.8
|
|
||||||
},
|
|
||||||
"active_kc": 1.15
|
|
||||||
},
|
|
||||||
"status": "completed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 201,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "گوجهفرنگی",
|
|
||||||
"light": "آفتاب کامل",
|
|
||||||
"watering": "منظم، هفتهای ۲ تا ۳ بار",
|
|
||||||
"soil": "لومی، غنی از مواد آلی",
|
|
||||||
"temperature": "۲۰ تا ۳۰ درجه سانتیگراد",
|
|
||||||
"planting_season": "بهار",
|
|
||||||
"harvest_time": "۷۰ تا ۹۰ روز پس از کاشت",
|
|
||||||
"spacing": "۴۵ تا ۶۰ سانتیمتر",
|
|
||||||
"fertilizer": "کود NPK متعادل",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "داده نامعتبر.",
|
|
||||||
"data": {
|
|
||||||
"name": [
|
|
||||||
"This field is required."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "گیاه با موفقیت حذف شد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"msg": "گیاه یافت نشد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "گوجهفرنگی",
|
|
||||||
"light": "آفتاب کامل",
|
|
||||||
"watering": "منظم، هفتهای ۲ تا ۳ بار",
|
|
||||||
"soil": "لومی، غنی از مواد آلی",
|
|
||||||
"temperature": "۲۰ تا ۳۰ درجه سانتیگراد",
|
|
||||||
"planting_season": "بهار",
|
|
||||||
"harvest_time": "۷۰ تا ۹۰ روز پس از کاشت",
|
|
||||||
"spacing": "۴۵ تا ۶۰ سانتیمتر",
|
|
||||||
"fertilizer": "کود NPK متعادل",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"msg": "گیاه یافت نشد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "گوجهفرنگی",
|
|
||||||
"light": "آفتاب کامل",
|
|
||||||
"watering": "منظم، هفتهای ۲ تا ۳ بار",
|
|
||||||
"soil": "لومی، غنی از مواد آلی",
|
|
||||||
"temperature": "۲۰ تا ۳۰ درجه سانتیگراد",
|
|
||||||
"planting_season": "بهار",
|
|
||||||
"harvest_time": "۷۰ تا ۹۰ روز پس از کاشت",
|
|
||||||
"spacing": "۴۵ تا ۶۰ سانتیمتر",
|
|
||||||
"fertilizer": "کود NPK متعادل",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "داده نامعتبر.",
|
|
||||||
"data": {
|
|
||||||
"name": [
|
|
||||||
"This field may not be blank."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"msg": "گیاه یافت نشد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "گوجهفرنگی",
|
|
||||||
"light": "آفتاب کامل",
|
|
||||||
"watering": "منظم، هفتهای ۲ تا ۳ بار",
|
|
||||||
"soil": "لومی، غنی از مواد آلی",
|
|
||||||
"temperature": "۲۰ تا ۳۰ درجه سانتیگراد",
|
|
||||||
"planting_season": "بهار",
|
|
||||||
"harvest_time": "۷۰ تا ۹۰ روز پس از کاشت",
|
|
||||||
"spacing": "۴۵ تا ۶۰ سانتیمتر",
|
|
||||||
"fertilizer": "کود NPK متعادل",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "داده نامعتبر.",
|
|
||||||
"data": {
|
|
||||||
"name": [
|
|
||||||
"This field may not be blank."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"msg": "گیاه یافت نشد.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "گوجهفرنگی",
|
|
||||||
"light": "آفتاب کامل",
|
|
||||||
"watering": "منظم، هفتهای ۲ تا ۳ بار",
|
|
||||||
"soil": "لومی، غنی از مواد آلی",
|
|
||||||
"temperature": "۲۰ تا ۳۰ درجه سانتیگراد",
|
|
||||||
"planting_season": "بهار",
|
|
||||||
"harvest_time": "۷۰ تا ۹۰ روز پس از کاشت",
|
|
||||||
"spacing": "۴۵ تا ۶۰ سانتیمتر",
|
|
||||||
"fertilizer": "کود NPK متعادل",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "نام گیاه الزامی است.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 503,
|
|
||||||
"msg": "سرویس API هنوز پیادهسازی نشده است.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "گوجهفرنگی",
|
|
||||||
"light": "آفتاب کامل",
|
|
||||||
"watering": "منظم، هفتهای ۲ تا ۳ بار",
|
|
||||||
"soil": "لومی، غنی از مواد آلی",
|
|
||||||
"temperature": "۲۰ تا ۳۰ درجه سانتیگراد",
|
|
||||||
"planting_season": "بهار",
|
|
||||||
"harvest_time": "۷۰ تا ۹۰ روز پس از کاشت",
|
|
||||||
"spacing": "۴۵ تا ۶۰ سانتیمتر",
|
|
||||||
"fertilizer": "کود NPK متعادل",
|
|
||||||
"created_at": "2025-03-20T10:00:00Z",
|
|
||||||
"updated_at": "2025-03-24T10:00:00Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"content_type": "text/plain; charset=utf-8",
|
|
||||||
"body": "سلام، برای بازیابی رطوبت خاک بهتر است آبیاری صبحگاهی را تنظیم کنید."
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "service_id نامعتبر است: unknown_service"
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "پارامتر query الزامی است."
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "برای این service_id، پارامتر user_id الزامی است."
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 202,
|
|
||||||
"msg": "تسک توصیه کودهی در صف قرار گرفت.",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-fert-123",
|
|
||||||
"status_url": "/api/rag/recommend/fertilization/rag-fert-123/status/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "پارامتر sensor_uuid الزامی است.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-fert-123",
|
|
||||||
"status": "FAILURE",
|
|
||||||
"error": "خطا در دریافت توصیه کودهی."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-fert-123",
|
|
||||||
"status": "PENDING",
|
|
||||||
"message": "تسک در صف یا یافت نشد."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-fert-123",
|
|
||||||
"status": "PROGRESS",
|
|
||||||
"progress": {
|
|
||||||
"message": "در حال پردازش توصیه کودهی..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-fert-123",
|
|
||||||
"status": "SUCCESS",
|
|
||||||
"result": {
|
|
||||||
"plan": {
|
|
||||||
"npkRatio": "20-20-20",
|
|
||||||
"amountPerHectare": "150 kg/ha",
|
|
||||||
"applicationMethod": "کودآبیاری در دو نوبت",
|
|
||||||
"applicationInterval": "هر ۱۰ روز",
|
|
||||||
"reasoning": "نیتروژن و پتاسیم خاک در محدوده متوسط است و گیاه در فاز رویشی نیاز تغذیهای بالاتری دارد."
|
|
||||||
},
|
|
||||||
"raw_response": "{\"plan\":{\"npkRatio\":\"20-20-20\"}}",
|
|
||||||
"status": "completed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 202,
|
|
||||||
"msg": "تسک توصیه آبیاری در صف قرار گرفت.",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-irr-123",
|
|
||||||
"status_url": "/api/rag/recommend/irrigation/rag-irr-123/status/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "پارامتر sensor_uuid الزامی است.",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-irr-123",
|
|
||||||
"status": "FAILURE",
|
|
||||||
"error": "خطا در دریافت توصیه آبیاری."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-irr-123",
|
|
||||||
"status": "PENDING",
|
|
||||||
"message": "تسک در صف یا یافت نشد."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-irr-123",
|
|
||||||
"status": "PROGRESS",
|
|
||||||
"progress": {
|
|
||||||
"message": "در حال پردازش توصیه آبیاری..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"task_id": "rag-irr-123",
|
|
||||||
"status": "SUCCESS",
|
|
||||||
"result": {
|
|
||||||
"plan": {
|
|
||||||
"frequencyPerWeek": 3,
|
|
||||||
"durationMinutes": 42,
|
|
||||||
"bestTimeOfDay": "صبح زود",
|
|
||||||
"moistureLevel": 68,
|
|
||||||
"warning": "در صورت بارش موثر، نوبت سوم این هفته را حذف کنید."
|
|
||||||
},
|
|
||||||
"raw_response": "{\"plan\":{\"frequencyPerWeek\":3,\"durationMinutes\":42}}",
|
|
||||||
"water_balance": {
|
|
||||||
"daily": [
|
|
||||||
{
|
|
||||||
"forecast_date": "2025-03-25",
|
|
||||||
"et0_mm": 4.7,
|
|
||||||
"etc_mm": 5.6,
|
|
||||||
"effective_rainfall_mm": 0.0,
|
|
||||||
"gross_irrigation_mm": 6.2,
|
|
||||||
"irrigation_timing": "06:00-08:00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"crop_profile": {
|
|
||||||
"kc_initial": 0.6,
|
|
||||||
"kc_mid": 1.15,
|
|
||||||
"kc_end": 0.8
|
|
||||||
},
|
|
||||||
"active_kc": 1.15
|
|
||||||
},
|
|
||||||
"status": "completed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 201,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"id": 3,
|
|
||||||
"code": "soil_moisture",
|
|
||||||
"name_fa": "رطوبت خاک",
|
|
||||||
"unit": "%",
|
|
||||||
"created_at": "2025-03-24T10:00:00Z",
|
|
||||||
"action": "added"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"msg": "داده نامعتبر.",
|
|
||||||
"data": {
|
|
||||||
"code": [
|
|
||||||
"This field is required."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user