This commit is contained in:
2026-05-05 23:54:44 +03:30
parent 4e28bacad6
commit 35f4d09225
12 changed files with 1291 additions and 29 deletions
+240
View File
@@ -0,0 +1,240 @@
# Farm Dashboard API Reference
این سند، API های `dashboard` را به‌صورت کامل توضیح می‌دهد و برای هر بخش مشخص می‌کند داده از کجا دریافت می‌شود.
## Endpoint ها
### 1) دریافت کارت‌های داشبورد
- **Method:** `GET`
- **Path:** `/api/farm-dashboard/`
- **View:** `dashboard/views.py:118`
- **URL config:** `dashboard/urls.py:5`
- **Query param الزامی:** `farm_uuid`
- **Auth:** `IsAuthenticated`
### 2) دریافت تنظیمات داشبورد
- **Method:** `GET`
- **Path:** `/api/farm-dashboard-config/`
- **View:** `dashboard/views.py:67`
- **URL config:** `dashboard/urls_config.py:5`
- **Query param الزامی:** `farm_uuid`
- **Auth:** `IsAuthenticated`
### 3) ویرایش تنظیمات داشبورد
- **Method:** `PATCH`
- **Path:** `/api/farm-dashboard-config/`
- **View:** `dashboard/views.py:67`
- **Body:** `farm_uuid` + هرکدام از `disabled_card_ids`، `row_order`، `enable_drag_reorder`
- **Auth:** `IsAuthenticated`
## نحوه شناسایی مزرعه
- مزرعه از طریق `farm_uuid` و مالک کاربر لاگین‌شده پیدا می‌شود.
- پیاده‌سازی در `dashboard/views.py:20` و `dashboard/views.py:22` است.
- اگر `farm_uuid` ارسال نشود یا مزرعه برای آن کاربر پیدا نشود، خطای validation برمی‌گردد.
## تنظیمات داشبورد
تنظیمات داشبورد per-farm در دیتابیس ذخیره می‌شود.
### فیلدها
- `disabled_card_ids`: لیست کارت‌های غیرفعال
- `row_order`: ترتیب ردیف‌ها
- `enable_drag_reorder`: فعال/غیرفعال بودن drag reorder
### مدل ذخیره‌سازی
- مدل: `FarmDashboardConfig`
- فایل: `dashboard/models.py:6`
- جدول: `farm_dashboard_configs`
### مقادیر پیش‌فرض
- از `dashboard/defaults.py:4` و `dashboard/defaults.py:30` می‌آید.
- کارت‌های معتبر در `dashboard/defaults.py:16`
- ردیف‌های معتبر در `dashboard/defaults.py:4`
### اعتبارسنجی
- serializer اصلی: `dashboard/serializers.py:6`
- serializer patch: `dashboard/serializers.py:43`
- `disabled_card_ids` فقط باید از `VALID_CARD_IDS` باشد.
- `row_order` باید تمام `VALID_ROW_IDS` را دقیقاً یک‌بار داشته باشد.
## نقطه تجمیع اصلی داده‌ها
تمام کارت‌ها در تابع زیر assemble می‌شوند:
- `dashboard/services.py:85`
این تابع خروجی چند app مختلف را جمع می‌کند و response نهایی dashboard را می‌سازد.
## منبع داده هر کارت
### `farmOverviewKpis`
- **از کجا ساخته می‌شود:** `dashboard/services.py:41`
- **نوع:** aggregator
- **منابع ورودی:**
- `farmHealthScore` از `crop_health.services.get_crop_health_summary_data`
- `waterStressIndex` از `water.services.get_water_stress_index_data`
- `avgSoilMoisture` از `device_hub.services.get_sensor_7_in_1_summary_data`
- `disease_risk` و `pest_risk` از `pest_detection.services.get_risk_summary_data`
- `yield_prediction_card` از `yield_harvest.services.get_yield_harvest_summary_data`
- **نکته مهم:** این کارت جدول یا مدل مستقل ندارد؛ از چند سرویس ترکیب می‌شود.
### `farmWeatherCard`
- **از کجا پر می‌شود:** `water/services.py:9`
- **مدل اصلی:** `WeatherForecastLog`
- **فایل مدل:** `water/models.py:8`
- **جدول:** `weather_forecast_logs`
- **منطق:** جدیدترین رکورد هواشناسی برای همان `farm` خوانده می‌شود.
### `farmAlertsTracker`
- **از کجا پر می‌شود:** `farm_alerts/services.py:379`
- **مدل اصلی:** `FarmAlert`
- **فایل مدل:** `farm_alerts/models.py:16`
- **جدول:** `farm_alerts`
- **منطق:** هشدارهای active مزرعه خوانده می‌شوند و از روی آن‌ها summary ساخته می‌شود.
- **نکته:** با اینکه مدل `FarmAlertTrackerSnapshot` هم وجود دارد در `farm_alerts/models.py:76`، endpoint فعلی کارت tracker مستقیم از `FarmAlert` می‌سازد، نه از snapshot.
### `sensorValuesList`
- **از کجا پر می‌شود:** `device_hub/services.py:495`
- **جزء داخلی:** `device_hub/services.py:334`
- **مدل‌ها:**
- `FarmDevice` در `device_hub/models.py:45`
- `SensorExternalRequestLog` در `device_hub/models.py:94`
- **جداول:**
- `farm_sensors`
- `sensor_external_request_logs`
- **منطق:**
- اول سنسور اصلی خاک مزرعه پیدا می‌شود.
- بعد history لاگ‌های همان device خوانده می‌شود.
- از payload لاگ‌ها، مقادیر سنسورها استخراج می‌شود.
### `sensorRadarChart`
- **از کجا پر می‌شود:** `device_hub/services.py:389`
- **منبع داده:** همان `SensorExternalRequestLog` و `FarmDevice`
- **منطق:** آخرین reading سنسور 7-in-1 گرفته می‌شود و بر اساس ideal range برای هر فیلد score ساخته می‌شود.
### `sensorComparisonChart`
- **از کجا پر می‌شود:** `device_hub/services.py:412`
- **منبع داده:** `SensorExternalRequestLog`
- **منطق:** history چند reading آخر برای رطوبت خاک گرفته می‌شود و series نمودار ساخته می‌شود.
### `anomalyDetectionCard`
- **از کجا پر می‌شود:** `device_hub/services.py:451`
- **منبع داده:** `SensorExternalRequestLog`
- **منطق:** آخرین reading با بازه‌های ideal مقایسه می‌شود و anomaly های out-of-range ساخته می‌شود.
- **نکته:** در app `farm_alerts` یک مدل `AnomalyDetection` در `farm_alerts/models.py:41` هم وجود دارد، اما dashboard فعلی این کارت را از آن مدل نمی‌خواند.
### `farmAlertsTimeline`
- **از کجا پر می‌شود:** `farm_alerts/services.py:410`
- **مدل اصلی:** `FarmAlert`
- **فایل مدل:** `farm_alerts/models.py:16`
- **جدول:** `farm_alerts`
- **منطق:** حداکثر 10 alert آخر مزرعه خوانده می‌شود.
### `waterNeedPrediction`
- **از کجا پر می‌شود:** `water/services.py:58`
- **مدل اصلی:** `IrrigationRecommendationRequest`
- **فایل مدل:** `irrigation/models.py:9`
- **جدول:** `irrigation_requests`
- **منطق:**
- از `response_payload` آخرین درخواست آبیاری، بخش `water_balance.daily` استخراج می‌شود.
- سپس `gross_irrigation_mm` ها تبدیل به series نمودار می‌شوند.
- **نکته:** این کارت در app `water` assemble می‌شود ولی source واقعی‌اش داده‌ی persisted آبیاری است.
### `harvestPredictionCard`
- **از کجا پر می‌شود:** `yield_harvest/services.py:7`
- **مدل اصلی:** `YieldHarvestPredictionLog`
- **فایل مدل:** `yield_harvest/models.py:8`
- **جدول:** `yield_harvest_prediction_logs`
- **منطق:** جدیدترین لاگ برداشت/عملکرد مزرعه خوانده می‌شود.
### `yieldPredictionChart`
- **از کجا پر می‌شود:** `yield_harvest/services.py:7`
- **مدل اصلی:** `YieldHarvestPredictionLog`
- **فایل مدل:** `yield_harvest/models.py:8`
- **جدول:** `yield_harvest_prediction_logs`
- **منطق:** `chart_data` از همان لاگ برداشت/عملکرد برگردانده می‌شود.
### `soilMoistureHeatmap`
- **از کجا پر می‌شود:** `device_hub/services.py:469`
- **منبع داده:** `SensorExternalRequestLog`
- **مدل کمکی device:** `FarmDevice`
- **منطق:** چند reading آخر رطوبت خاک به فرمت heatmap/chart تبدیل می‌شود.
### `ndviHealthCard`
- **از کجا پر می‌شود:** `crop_health/services.py:6`
- **منبع داده فعلی:** mock data
- **فایل:** `crop_health/mock_data.py` از طریق `crop_health/services.py:3`
- **منطق:** فعلاً از دیتابیس یا external log خوانده نمی‌شود؛ مستقیم از mock برمی‌گردد.
### `recommendationsList`
- **از کجا ساخته می‌شود:** `dashboard/services.py:54`
- **نوع:** aggregator
- **منابع ورودی:**
- recommendationهای ذخیره‌شده در `Recommendation` از `farm_alerts/services.py:459`
- پیشنهاد آبیاری از `irrigation/services.py:289`
- پیشنهاد کوددهی از `fertilization/services.py:79`
- بازه برداشت از `yield_harvest.services.get_yield_harvest_summary_data`
- **مدل‌های اصلی:**
- `Recommendation` در `farm_alerts/models.py:59`
- `IrrigationRecommendationRequest` در `irrigation/models.py:9`
- `FertilizationRecommendationRequest` در `fertilization/models.py:9`
- `YieldHarvestPredictionLog` در `yield_harvest/models.py:8`
- **نکته:** این کارت داده چند domain مختلف را یکی می‌کند و duplicate titleها را حذف می‌کند.
### `economicOverview`
- **از کجا پر می‌شود:** `economic_overview/services.py:7`
- **مدل اصلی:** `EconomicOverviewLog`
- **فایل مدل:** `economic_overview/models.py:8`
- **جدول:** `economic_overview_logs`
- **منطق:** آخرین لاگ اقتصادی مزرعه خوانده می‌شود.
## منابعی که فعلاً mock هستند
این بخش مهم است، چون user خواسته بداند اطلاعات از کجا می‌آید:
- `ndviHealthCard` از mock می‌آید: `crop_health/services.py:6`
- `farmHealthScore` که داخل `farmOverviewKpis` استفاده می‌شود هم از mock می‌آید: `crop_health/services.py:6`
- `disease_risk` و `pest_risk` که داخل `farmOverviewKpis` استفاده می‌شوند از mock می‌آیند: `pest_detection/services.py:6`
## منابعی که از دیتابیس می‌آیند
- تنظیمات dashboard از `FarmDashboardConfig`
- weather از `WeatherForecastLog`
- alerts/timeline از `FarmAlert`
- recommendationهای ذخیره‌شده از `Recommendation`
- داده آبیاری از `IrrigationRecommendationRequest`
- داده کوددهی برای recommendation card از `FertilizationRecommendationRequest`
- برداشت/عملکرد از `YieldHarvestPredictionLog`
- overview اقتصادی از `EconomicOverviewLog`
- سنسورها از `FarmDevice` و `SensorExternalRequestLog`
## وابستگی بین app ها در dashboard
تجمیع dashboard در `dashboard/services.py:85` به این app ها وابسته است:
- `water`
- `crop_health`
- `economic_overview`
- `farm_alerts`
- `fertilization`
- `irrigation`
- `pest_detection`
- `device_hub`
- `yield_harvest`
## نمونه flow برای `GET /api/farm-dashboard/`
1. کاربر `farm_uuid` را می‌فرستد.
2. در `dashboard/views.py:127` مزرعه متعلق به user پیدا می‌شود.
3. `dashboard/services.py:85` صدا زده می‌شود.
4. این تابع به سرویس‌های appهای مختلف call می‌زند.
5. هر سرویس یا از DB می‌خواند یا از mock/template.
6. پاسخ نهایی به‌صورت یک object شامل تمام cardها برمی‌گردد.
## نکات مهم عملی
- endpoint کارت‌ها فقط config را برنمی‌گرداند؛ payload کامل تمام cardها را یکجا برمی‌گرداند.
- config dashboard از خود کارت‌ها جداست و در endpoint جداگانه مدیریت می‌شود.
- بعضی کارت‌ها production data دارند، بعضی transitional هستند، و بعضی هنوز mock دارند.
- اگر برای مزرعه داده‌ای در بعضی جدول‌ها نباشد، معمولاً fallback/template خالی برمی‌گردد.
## فایل‌های مرجع مهم
- `dashboard/views.py:67`
- `dashboard/views.py:118`
- `dashboard/services.py:85`
- `dashboard/defaults.py:4`
- `dashboard/serializers.py:6`
- `dashboard/models.py:6`
- `docs/dashboard_card_service_map.md:1`
+161 -7
View File
@@ -80,6 +80,153 @@
## APIهای پیشنهادی
## راهنمای `device_code`
در این معماری باید بین این سه مفهوم تفاوت روشن باشد:
- `physical_device_uuid`: شناسه خودِ دستگاه ثبت‌شده روی مزرعه
- `device_catalog.uuid`: شناسه رکورد catalog
- `device_code`: مقدار متنی فیلد `DeviceCatalog.code` مثل `soil_sensor_v2` یا `irrigation_valve_v1`
### `device_code` را از کجا می‌گیریم؟
دو راه اصلی برای پیدا کردن `device_code`های یک دستگاه وجود دارد:
#### 1) از جزئیات device
در پاسخ این endpoint:
```http
GET /api/device-hub/devices/{physical_device_uuid}/?device_code=<device_code>
```
فیلدهای زیر برمی‌گردند:
- `data.device_catalog.code`
- `data.device_catalogs[].code`
یعنی frontend می‌تواند codeهای attachشده به device را از همین پاسخ بخواند.
#### 2) از endpoint اختصاصی لیست codeها
```http
GET /api/device-hub/devices/{physical_device_uuid}/device-codes/
```
پاسخ نمونه:
```json
{
"code": 200,
"msg": "success",
"data": {
"physical_device_uuid": "device-uuid",
"device_codes": ["soil_sensor_v2", "air_sensor_v1"]
}
}
```
این endpoint برای وقتی مناسب است که frontend فقط می‌خواهد بداند این device به چه `device_code`هایی وصل است.
### `device_code` را کجا باید ارسال کنیم؟
`device_code` همیشه لازم نیست. بسته به endpoint یکی از این حالت‌ها را دارد:
#### الف) در query string
برای endpointهایی که خروجی آن‌ها باید بر اساس یکی از catalogهای attachشده انتخاب شود:
```http
GET /api/device-hub/devices/{physical_device_uuid}/?device_code=soil_sensor_v2
GET /api/device-hub/devices/{physical_device_uuid}/latest/?device_code=soil_sensor_v2
GET /api/device-hub/devices/{physical_device_uuid}/summary/?device_code=soil_sensor_v2
GET /api/device-hub/devices/{physical_device_uuid}/values-list/?device_code=soil_sensor_v2&range=7d
GET /api/device-hub/devices/{physical_device_uuid}/comparison-chart/?device_code=soil_sensor_v2&range=7d
GET /api/device-hub/devices/{physical_device_uuid}/radar-chart/?device_code=soil_sensor_v2&range=7d
GET /api/device-hub/devices/{physical_device_uuid}/logs/?device_code=soil_sensor_v2&page=1&page_size=20
```
#### ب) در body درخواست
برای endpoint command:
```http
POST /api/device-hub/devices/{physical_device_uuid}/commands/
```
نمونه body:
```json
{
"device_code": "irrigation_valve_v1",
"command": "open",
"payload": {
"duration_seconds": 120
}
}
```
#### ج) endpointهایی که اصلاً `device_code` نمی‌خواهند
این endpoint فقط با `physical_device_uuid` کار می‌کند:
```http
GET /api/device-hub/devices/{physical_device_uuid}/device-codes/
```
و endpointهای catalog-level هم معمولاً `device_code` لازم ندارند:
```http
GET /api/device-hub/catalog/
```
### چه زمانی `device_code` اجباری است؟
وقتی یک `FarmDevice` ممکن است به چند catalog وصل باشد، backend بدون `device_code` نمی‌تواند بفهمد باید:
- mapping کدام catalog را اعمال کند
- widgetهای کدام catalog را برگرداند
- لاگ را بر اساس کدام catalog فیلتر کند
- command را برای کدام نوع device validate کند
پس در endpointهای data/summary/chart/logs/commands باید `device_code` صریح ارسال شود.
### `device_code` دقیقاً باید چه مقداری باشد؟
باید مقدار فیلد `DeviceCatalog.code` ارسال شود، نه:
- `name`
- `uuid`
- `physical_device_uuid`
مثال درست:
```text
soil_sensor_v2
air_sensor_v1
irrigation_valve_v1
```
مثال اشتباه:
```text
Soil Sensor V2
11111111-1111-1111-1111-111111111111
22222222-2222-2222-2222-222222222222
```
### اگر `device_code` اشتباه باشد چه می‌شود؟
اگر `device_code` به آن device attach نشده باشد، backend باید validation error برگرداند. معمولاً چیزی شبیه این:
```json
{
"device_code": [
"Device code is not attached to this farm device."
]
}
```
### 1) لیست دیوایس‌ها
```http
@@ -98,9 +245,14 @@ GET /api/device-hub/catalog/
### 2) جزئیات یک دیوایس ثبت‌شده روی مزرعه
```http
GET /api/device-hub/devices/{physical_device_uuid}/
GET /api/device-hub/devices/{physical_device_uuid}/?device_code=soil_sensor_v1
```
نکته:
- در این endpoint، `device_code` باید در query string ارسال شود.
- اگر device فقط یک catalog داشته باشد، از نظر معماری باز هم بهتر است frontend آن را صریح بفرستد.
پاسخ نمونه:
```json
@@ -129,7 +281,7 @@ GET /api/device-hub/devices/{physical_device_uuid}/
### 3) آخرین داده‌ی یک device
```http
GET /api/device-hub/devices/{physical_device_uuid}/latest/
GET /api/device-hub/devices/{physical_device_uuid}/latest/?device_code=soil_sensor_v1
```
کاربرد:
@@ -143,7 +295,7 @@ GET /api/device-hub/devices/{physical_device_uuid}/latest/
### 4) summary داینامیک برای یک device
```http
GET /api/device-hub/devices/{physical_device_uuid}/summary/
GET /api/device-hub/devices/{physical_device_uuid}/summary/?device_code=soil_sensor_v1
```
کاربرد:
@@ -156,7 +308,7 @@ GET /api/device-hub/devices/{physical_device_uuid}/summary/
### 5) نمودار مقایسه‌ای داینامیک
```http
GET /api/device-hub/devices/{physical_device_uuid}/comparison-chart/?range=7d
GET /api/device-hub/devices/{physical_device_uuid}/comparison-chart/?device_code=soil_sensor_v1&range=7d
```
---
@@ -164,7 +316,7 @@ GET /api/device-hub/devices/{physical_device_uuid}/comparison-chart/?range=7d
### 6) نمودار رادار داینامیک
```http
GET /api/device-hub/devices/{physical_device_uuid}/radar-chart/?range=7d
GET /api/device-hub/devices/{physical_device_uuid}/radar-chart/?device_code=soil_sensor_v1&range=7d
```
---
@@ -172,7 +324,7 @@ GET /api/device-hub/devices/{physical_device_uuid}/radar-chart/?range=7d
### 7) values list داینامیک
```http
GET /api/device-hub/devices/{physical_device_uuid}/values-list/?range=7d
GET /api/device-hub/devices/{physical_device_uuid}/values-list/?device_code=soil_sensor_v1&range=7d
```
---
@@ -180,7 +332,7 @@ GET /api/device-hub/devices/{physical_device_uuid}/values-list/?range=7d
### 8) دریافت history خام
```http
GET /api/device-hub/devices/{physical_device_uuid}/logs/?page=1&page_size=20
GET /api/device-hub/devices/{physical_device_uuid}/logs/?device_code=soil_sensor_v1&page=1&page_size=20
```
این endpoint برای debug و audit خیلی مهم است.
@@ -320,6 +472,7 @@ payload نمونه:
```json
{
"device_code": "irrigation_valve_v1",
"command": "turn_on",
"payload": {
"duration_seconds": 120
@@ -450,6 +603,7 @@ serializerها مخصوص 7-in-1 هستند.
```python
urlpatterns = [
path("catalog/", DeviceCatalogListView.as_view(), name="device-catalog-list"),
path("devices/<uuid:physical_device_uuid>/device-codes/", DeviceCodeListView.as_view(), name="device-code-list"),
path("devices/<uuid:physical_device_uuid>/", DeviceDetailView.as_view(), name="device-detail"),
path("devices/<uuid:physical_device_uuid>/latest/", DeviceLatestPayloadView.as_view(), name="device-latest-payload"),
path("devices/<uuid:physical_device_uuid>/summary/", DeviceSummaryView.as_view(), name="device-summary"),