UPDATE
This commit is contained in:
Binary file not shown.
@@ -38,6 +38,58 @@ def _update_kpi(card_lookup, card_data):
|
||||
card_lookup[card_id]["details"] = details
|
||||
|
||||
|
||||
def _build_quality_score_card(yield_summary):
|
||||
quality_card = {
|
||||
"id": "quality_score",
|
||||
"title": "امتیاز کیفیت",
|
||||
"subtitle": "برآورد کیفیت محصول",
|
||||
"stats": "۵۹",
|
||||
"avatarColor": "info",
|
||||
"avatarIcon": "tabler-stars",
|
||||
"chipText": "متوسط",
|
||||
"chipColor": "warning",
|
||||
}
|
||||
|
||||
chart_summary = yield_summary.get("yield_prediction_chart", {}).get("summary", [])
|
||||
if not isinstance(chart_summary, list):
|
||||
return quality_card
|
||||
|
||||
for item in chart_summary:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
title = str(item.get("title", "")).strip()
|
||||
if "کیفیت" not in title:
|
||||
continue
|
||||
amount = item.get("amount")
|
||||
subtitle = item.get("subtitle")
|
||||
if amount not in (None, ""):
|
||||
quality_card["stats"] = str(amount)
|
||||
if subtitle:
|
||||
quality_card["chipText"] = str(subtitle)
|
||||
quality_card["chipColor"] = "warning"
|
||||
return quality_card
|
||||
|
||||
return quality_card
|
||||
|
||||
|
||||
def _build_days_until_harvest_card(yield_summary):
|
||||
harvest_card = yield_summary.get("harvest_prediction_card", {})
|
||||
days_until = harvest_card.get("daysUntil")
|
||||
card = {
|
||||
"id": "days_until_harvest",
|
||||
"title": "روز تا برداشت",
|
||||
"subtitle": "زمان باقیمانده تا پنجره برداشت",
|
||||
"stats": "۱۳۵",
|
||||
"avatarColor": "warning",
|
||||
"avatarIcon": "tabler-calendar-event",
|
||||
"chipText": "برنامه ریزی",
|
||||
"chipColor": "success",
|
||||
}
|
||||
if days_until is not None:
|
||||
card["stats"] = str(days_until)
|
||||
return card
|
||||
|
||||
|
||||
def _build_overview_kpis(base_cards, crop_health_summary, water_stress_index, avg_soil_moisture, risk_summary, yield_summary):
|
||||
kpis = [crop_health_summary["farmHealthScore"], water_stress_index, avg_soil_moisture, *deepcopy(base_cards["kpis"])]
|
||||
card_lookup = {item["id"]: item for item in kpis}
|
||||
@@ -47,6 +99,8 @@ def _build_overview_kpis(base_cards, crop_health_summary, water_stress_index, av
|
||||
_update_kpi(card_lookup, risk_summary.get("disease_risk", {}))
|
||||
_update_kpi(card_lookup, risk_summary.get("pest_risk", {}))
|
||||
_update_kpi(card_lookup, yield_summary.get("yield_prediction_card", {}))
|
||||
_update_kpi(card_lookup, _build_quality_score_card(yield_summary))
|
||||
_update_kpi(card_lookup, _build_days_until_harvest_card(yield_summary))
|
||||
|
||||
return {"kpis": kpis}
|
||||
|
||||
|
||||
+37
-17
@@ -10,31 +10,51 @@ FARM_OVERVIEW_KPIS = {
|
||||
{
|
||||
"id": "disease_risk",
|
||||
"title": "ریسک بیماری",
|
||||
"subtitle": "۷ روز اخیر",
|
||||
"subtitle": "پیش بینی هوشمند",
|
||||
"stats": "پایین",
|
||||
"avatarColor": "success",
|
||||
"avatarIcon": "tabler-bug",
|
||||
"chipText": "5%",
|
||||
"chipColor": "success",
|
||||
},
|
||||
{
|
||||
"id": "yield_prediction",
|
||||
"title": "پیشبینی عملکرد",
|
||||
"subtitle": "این فصل",
|
||||
"stats": "42 تن",
|
||||
"avatarColor": "secondary",
|
||||
"avatarIcon": "tabler-chart-bar",
|
||||
"chipText": "+8%",
|
||||
"avatarIcon": "tabler-biohazard",
|
||||
"chipText": "32%",
|
||||
"chipColor": "success",
|
||||
},
|
||||
{
|
||||
"id": "pest_risk",
|
||||
"title": "ریسک آفات",
|
||||
"subtitle": "پیشبینی هوشمند",
|
||||
"stats": "15%",
|
||||
"subtitle": "پیش بینی هوشمند",
|
||||
"stats": "پایین",
|
||||
"avatarColor": "success",
|
||||
"avatarIcon": "tabler-bug",
|
||||
"chipText": "22%",
|
||||
"chipColor": "success",
|
||||
},
|
||||
{
|
||||
"id": "yield_prediction",
|
||||
"title": "عملکرد پیش بینی شده",
|
||||
"subtitle": "پیش بینی عملکرد این مزرعه",
|
||||
"stats": "۰ تن",
|
||||
"avatarColor": "primary",
|
||||
"avatarIcon": "tabler-chart-arcs",
|
||||
"chipText": "نیازمند بررسی",
|
||||
"chipColor": "warning",
|
||||
},
|
||||
{
|
||||
"id": "quality_score",
|
||||
"title": "امتیاز کیفیت",
|
||||
"subtitle": "برآورد کیفیت محصول",
|
||||
"stats": "۵۹",
|
||||
"avatarColor": "info",
|
||||
"avatarIcon": "tabler-stars",
|
||||
"chipText": "متوسط",
|
||||
"chipColor": "warning",
|
||||
},
|
||||
{
|
||||
"id": "days_until_harvest",
|
||||
"title": "روز تا برداشت",
|
||||
"subtitle": "زمان باقیمانده تا پنجره برداشت",
|
||||
"stats": "۱۳۵",
|
||||
"avatarColor": "warning",
|
||||
"avatarIcon": "tabler-bug-off",
|
||||
"chipText": "تحت نظر",
|
||||
"avatarIcon": "tabler-calendar-event",
|
||||
"chipText": "برنامه ریزی",
|
||||
"chipColor": "warning",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -153,6 +153,12 @@ class FarmDashboardCardsViewTests(DashboardBaseTestCase):
|
||||
self.assertIn("economicOverview", response.data["data"])
|
||||
self.assertEqual(response.data["data"]["farmOverviewKpis"]["kpis"][0]["id"], "farm_health_score")
|
||||
self.assertEqual(response.data["data"]["farmOverviewKpis"]["kpis"][2]["id"], "avg_soil_moisture")
|
||||
kpi_ids = [item["id"] for item in response.data["data"]["farmOverviewKpis"]["kpis"]]
|
||||
self.assertIn("disease_risk", kpi_ids)
|
||||
self.assertIn("pest_risk", kpi_ids)
|
||||
self.assertIn("yield_prediction", kpi_ids)
|
||||
self.assertIn("quality_score", kpi_ids)
|
||||
self.assertIn("days_until_harvest", kpi_ids)
|
||||
|
||||
def test_get_requires_farm_uuid(self):
|
||||
request = self.factory.get("/api/farm-dashboard/")
|
||||
|
||||
@@ -0,0 +1,734 @@
|
||||
# Device Hub API Guide
|
||||
|
||||
این فایل مستند API های تعریفشده در `device_hub/urls.py` را توضیح میدهد. مسیر پایه این API ها طبق `config/urls.py` برابر است با:
|
||||
|
||||
- `api/device-hub/`
|
||||
|
||||
بیشتر endpointها نیاز به احراز هویت کاربر دارند و معمولاً با ساختار زیر پاسخ میدهند:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
## نحوه ارتباط با API ها
|
||||
|
||||
### 1) احراز هویت
|
||||
|
||||
- برای بیشتر endpointها باید کاربر لاگین باشد.
|
||||
- معمولاً توکن را در هدر `Authorization` ارسال میکنید.
|
||||
- نمونه:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 2) آدرس پایه
|
||||
|
||||
اگر دامنه پروژه مثلاً `https://example.com` باشد، آدرس کامل endpointها به این صورت است:
|
||||
|
||||
- `https://example.com/api/device-hub/catalog/`
|
||||
- `https://example.com/api/device-hub/devices/<physical_device_uuid>/latest/?device_code=<code>`
|
||||
|
||||
### 3) پارامترهای مهم
|
||||
|
||||
- `physical_device_uuid`: شناسه فیزیکی دستگاه
|
||||
- `device_code`: کد نوع device داخل catalog
|
||||
- `range`: بازه زمانی (`1h`, `24h`, `7d`, `30d`, `today` بسته به endpoint)
|
||||
- `page` و `page_size`: برای صفحهبندی لاگها
|
||||
|
||||
---
|
||||
|
||||
## 1. دریافت لیست کاتالوگ دستگاهها
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/catalog/`
|
||||
- `GET /api/device-hub/`
|
||||
|
||||
### کاربرد
|
||||
|
||||
برای گرفتن لیست همه device catalogها استفاده میشود.
|
||||
|
||||
### درخواست نمونه
|
||||
|
||||
```bash
|
||||
curl -X GET "https://example.com/api/device-hub/catalog/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"uuid": "11111111-1111-1111-1111-111111111111",
|
||||
"code": "soil_sensor_v2",
|
||||
"name": "Soil Sensor V2",
|
||||
"description": "",
|
||||
"device_communication_type": "output_only",
|
||||
"customizable_fields": [],
|
||||
"supported_power_sources": [],
|
||||
"returned_data_fields": ["soil_moisture", "soil_temperature"],
|
||||
"payload_mapping": {
|
||||
"soil_moisture": ["moisture", "soil_moisture"],
|
||||
"soil_temperature": ["temperature", "soil_temperature"]
|
||||
},
|
||||
"display_schema": {},
|
||||
"supported_widgets": ["values_list", "comparison_chart", "radar_chart"],
|
||||
"commands_schema": [],
|
||||
"capabilities": [],
|
||||
"sample_payload": {},
|
||||
"is_active": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### فیلدهای مهم پاسخ
|
||||
|
||||
- `device_communication_type`: نوع ارتباط دستگاه (`output_only` یا `input_only`)
|
||||
- `returned_data_fields`: دادههایی که device برمیگرداند
|
||||
- `payload_mapping`: نگاشت کلیدهای payload خام به فیلدهای نرمال
|
||||
- `supported_widgets`: ویجتهایی که فرانت میتواند نمایش دهد
|
||||
- `commands_schema`: لیست commandهای قابل ارسال برای deviceهای commandable
|
||||
|
||||
---
|
||||
|
||||
## 2. جزئیات یک دستگاه
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/devices/<physical_device_uuid>/?device_code=<device_code>`
|
||||
|
||||
### کاربرد
|
||||
|
||||
اطلاعات کلی دستگاه ثبتشده در فارم را برمیگرداند.
|
||||
|
||||
### Query Params
|
||||
|
||||
- `device_code` اجباری
|
||||
|
||||
### درخواست نمونه
|
||||
|
||||
```bash
|
||||
curl -X GET "https://example.com/api/device-hub/devices/22222222-2222-2222-2222-222222222222/?device_code=soil_sensor_v2" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"uuid": "33333333-3333-3333-3333-333333333333",
|
||||
"physical_device_uuid": "22222222-2222-2222-2222-222222222222",
|
||||
"name": "Soil Device 1",
|
||||
"sensor_type": "soil",
|
||||
"is_active": true,
|
||||
"specifications": {},
|
||||
"power_source": {},
|
||||
"device_catalog": {
|
||||
"uuid": "11111111-1111-1111-1111-111111111111",
|
||||
"code": "soil_sensor_v2",
|
||||
"name": "Soil Sensor V2",
|
||||
"description": "",
|
||||
"device_communication_type": "output_only",
|
||||
"customizable_fields": [],
|
||||
"supported_power_sources": [],
|
||||
"returned_data_fields": ["soil_moisture", "soil_temperature"],
|
||||
"payload_mapping": {},
|
||||
"display_schema": {},
|
||||
"supported_widgets": ["values_list", "comparison_chart", "radar_chart"],
|
||||
"commands_schema": [],
|
||||
"capabilities": [],
|
||||
"sample_payload": {},
|
||||
"is_active": true
|
||||
},
|
||||
"device_catalogs": [],
|
||||
"last_payload_at": "2025-01-01T10:00:00Z",
|
||||
"created_at": "2025-01-01T09:00:00Z",
|
||||
"updated_at": "2025-01-01T09:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### نکته
|
||||
|
||||
- اگر `physical_device_uuid` متعلق به کاربر نباشد، خطای 400 با متن `Device not found.` برمیگردد.
|
||||
- اگر `device_code` به این دستگاه attach نشده باشد، خطای validation دریافت میکنید.
|
||||
|
||||
---
|
||||
|
||||
## 3. آخرین payload دستگاه
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/devices/<physical_device_uuid>/latest/?device_code=<device_code>`
|
||||
|
||||
### کاربرد
|
||||
|
||||
آخرین payload خام و نرمالشده دستگاه را میدهد.
|
||||
|
||||
### Query Params
|
||||
|
||||
- `device_code` اجباری
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"physical_device_uuid": "22222222-2222-2222-2222-222222222222",
|
||||
"device_code": "soil_sensor_v2",
|
||||
"device_catalog_code": "soil_sensor_v2",
|
||||
"raw_payload": {
|
||||
"moisture": 52.4,
|
||||
"temperature": 23.1
|
||||
},
|
||||
"normalized_payload": {
|
||||
"soil_moisture": 52.4,
|
||||
"soil_temperature": 23.1
|
||||
},
|
||||
"readings": {
|
||||
"soil_moisture": 52.4,
|
||||
"soil_temperature": 23.1
|
||||
},
|
||||
"created_at": "2025-01-01T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### معنی فیلدها
|
||||
|
||||
- `raw_payload`: داده خامی که از لاگ ذخیرهشده آمده
|
||||
- `normalized_payload`: داده تبدیلشده بر اساس `payload_mapping`
|
||||
- `readings`: مقادیر قابل نمایش برای UI
|
||||
|
||||
---
|
||||
|
||||
## 4. خلاصه دستگاه
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/devices/<physical_device_uuid>/summary/?device_code=<device_code>`
|
||||
|
||||
### کاربرد
|
||||
|
||||
یک summary مناسب UI برمیگرداند؛ مثلاً ویجتهای قابل نمایش، values list، chartها و commandها.
|
||||
|
||||
### Query Params
|
||||
|
||||
- `device_code` اجباری
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"sensor": {
|
||||
"name": "Soil Device 1",
|
||||
"physicalDeviceUuid": "22222222-2222-2222-2222-222222222222",
|
||||
"sensorCatalogCode": "soil_sensor_v2",
|
||||
"updatedAt": "2025-01-01T10:00:00Z"
|
||||
},
|
||||
"supportedWidgets": ["values_list", "comparison_chart", "radar_chart"],
|
||||
"sensorValuesList": {
|
||||
"sensor": {
|
||||
"name": "Soil Device 1",
|
||||
"physicalDeviceUuid": "22222222-2222-2222-2222-222222222222",
|
||||
"sensorCatalogCode": "soil_sensor_v2",
|
||||
"updatedAt": "2025-01-01T10:00:00Z"
|
||||
},
|
||||
"sensors": [
|
||||
{
|
||||
"id": "soil_moisture",
|
||||
"title": "رطوبت خاک",
|
||||
"subtitle": "45-65%",
|
||||
"trendNumber": 52.4,
|
||||
"trend": "normal",
|
||||
"unit": "%"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### نکته
|
||||
|
||||
- شکل دقیق `data` بسته به `supported_widgets` و نوع catalog تغییر میکند.
|
||||
- اگر history وجود نداشته باشد، معمولاً خطای 400 برمیگردد.
|
||||
|
||||
---
|
||||
|
||||
## 5. نمودار مقایسهای دستگاه
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/devices/<physical_device_uuid>/comparison-chart/?device_code=<device_code>&range=7d`
|
||||
|
||||
### Query Params
|
||||
|
||||
- `device_code` اجباری
|
||||
- `range` اختیاری: `7d` یا `30d`
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"name": "Moisture",
|
||||
"data": [50.0, 51.2, 52.4]
|
||||
},
|
||||
{
|
||||
"name": "Temperature",
|
||||
"data": [22.0, 22.4, 23.1]
|
||||
}
|
||||
],
|
||||
"categories": ["شنبه", "یکشنبه", "دوشنبه"],
|
||||
"currentValue": 52.4,
|
||||
"vsLastWeek": "+3.1%"
|
||||
}
|
||||
```
|
||||
|
||||
### نکته
|
||||
|
||||
- این endpoint برخلاف بعضی endpointهای دیگر wrapper `code/msg` ندارد و مستقیم data chart را برمیگرداند.
|
||||
|
||||
---
|
||||
|
||||
## 6. لیست مقادیر دستگاه
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/devices/<physical_device_uuid>/values-list/?device_code=<device_code>&range=7d`
|
||||
|
||||
### Query Params
|
||||
|
||||
- `device_code` اجباری
|
||||
- `range` اختیاری: `1h`، `24h`، `7d`
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"sensors": [
|
||||
{
|
||||
"title": "Moisture",
|
||||
"subtitle": "45-65%",
|
||||
"trendNumber": 52.4,
|
||||
"trend": "positive",
|
||||
"unit": "%"
|
||||
},
|
||||
{
|
||||
"title": "Temperature",
|
||||
"subtitle": "18-28°C",
|
||||
"trendNumber": 23.1,
|
||||
"trend": "positive",
|
||||
"unit": "°C"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### نکته
|
||||
|
||||
- این endpoint هم مستقیم JSON داده را برمیگرداند و wrapper `code/msg` ندارد.
|
||||
|
||||
---
|
||||
|
||||
## 7. رادار چارت دستگاه
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/devices/<physical_device_uuid>/radar-chart/?device_code=<device_code>&range=7d`
|
||||
|
||||
### Query Params
|
||||
|
||||
- `device_code` اجباری
|
||||
- `range` اختیاری: `today`، `7d`، `30d`
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"labels": ["Moisture", "Temperature", "PH", "EC"],
|
||||
"series": [
|
||||
{
|
||||
"name": "Current",
|
||||
"data": [52.4, 23.1, 6.7, 1.1]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### نکته
|
||||
|
||||
- این endpoint هم مستقیم data را برمیگرداند.
|
||||
|
||||
---
|
||||
|
||||
## 8. لاگهای دستگاه
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/devices/<physical_device_uuid>/logs/?device_code=<device_code>&page=1&page_size=20`
|
||||
|
||||
### Query Params
|
||||
|
||||
- `device_code` اجباری
|
||||
- `page` اختیاری، پیشفرض `1`
|
||||
- `page_size` اختیاری، پیشفرض `20`، حداکثر `100`
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"data": [
|
||||
{
|
||||
"id": 10,
|
||||
"farm_uuid": "44444444-4444-4444-4444-444444444444",
|
||||
"sensor_catalog_uuid": "11111111-1111-1111-1111-111111111111",
|
||||
"physical_device_uuid": "22222222-2222-2222-2222-222222222222",
|
||||
"farm_device": {
|
||||
"uuid": "33333333-3333-3333-3333-333333333333",
|
||||
"sensor_catalog_uuid": "11111111-1111-1111-1111-111111111111",
|
||||
"device_catalogs": [],
|
||||
"physical_device_uuid": "22222222-2222-2222-2222-222222222222",
|
||||
"name": "Soil Device 1",
|
||||
"sensor_type": "soil",
|
||||
"is_active": true,
|
||||
"specifications": {},
|
||||
"power_source": {},
|
||||
"created_at": "2025-01-01T09:00:00Z",
|
||||
"updated_at": "2025-01-01T09:00:00Z"
|
||||
},
|
||||
"sensor_catalog": {
|
||||
"uuid": "11111111-1111-1111-1111-111111111111",
|
||||
"code": "soil_sensor_v2",
|
||||
"name": "Soil Sensor V2",
|
||||
"description": "",
|
||||
"device_communication_type": "output_only",
|
||||
"customizable_fields": [],
|
||||
"supported_power_sources": [],
|
||||
"returned_data_fields": [],
|
||||
"payload_mapping": {},
|
||||
"display_schema": {},
|
||||
"supported_widgets": [],
|
||||
"commands_schema": [],
|
||||
"capabilities": [],
|
||||
"sample_payload": {},
|
||||
"is_active": true,
|
||||
"created_at": "2025-01-01T09:00:00Z",
|
||||
"updated_at": "2025-01-01T09:00:00Z"
|
||||
},
|
||||
"payload": {
|
||||
"moisture": 52.4,
|
||||
"temperature": 23.1
|
||||
},
|
||||
"created_at": "2025-01-01T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### کاربرد
|
||||
|
||||
- برای history دستگاه
|
||||
- برای نمایش payloadهای دریافتشده از device
|
||||
- برای debug یا audit
|
||||
|
||||
---
|
||||
|
||||
## 9. ارسال command به دستگاه
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `POST /api/device-hub/devices/<physical_device_uuid>/commands/`
|
||||
|
||||
### کاربرد
|
||||
|
||||
برای deviceهایی که `input_only` یا commandable هستند، command ارسال میکند.
|
||||
|
||||
### Body
|
||||
|
||||
```json
|
||||
{
|
||||
"device_code": "valve_v1",
|
||||
"command": "open",
|
||||
"payload": {
|
||||
"duration_seconds": 120
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### درخواست نمونه
|
||||
|
||||
```bash
|
||||
curl -X POST "https://example.com/api/device-hub/devices/22222222-2222-2222-2222-222222222222/commands/" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_code": "valve_v1",
|
||||
"command": "open",
|
||||
"payload": {
|
||||
"duration_seconds": 120
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "command accepted",
|
||||
"data": {
|
||||
"physical_device_uuid": "22222222-2222-2222-2222-222222222222",
|
||||
"command": "open",
|
||||
"status": "accepted"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### نکات
|
||||
|
||||
- `device_code` باید جزو catalogهای متصل به آن device باشد.
|
||||
- اگر command یا device code معتبر نباشد، خطای 400 برمیگردد.
|
||||
|
||||
---
|
||||
|
||||
## 10. خلاصه سنسور 7in1
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/summary/?farm_uuid=<farm_uuid>`
|
||||
|
||||
### کاربرد
|
||||
|
||||
خلاصهای از سنسور اصلی مزرعه برای UI برمیگرداند.
|
||||
|
||||
### Query Params
|
||||
|
||||
- `farm_uuid` اجباری
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "OK",
|
||||
"data": {
|
||||
"sensor": {
|
||||
"name": "Main Soil Sensor",
|
||||
"physicalDeviceUuid": "22222222-2222-2222-2222-222222222222",
|
||||
"sensorCatalogCode": "sensor-7-in-1",
|
||||
"updatedAt": "2025-01-01T10:00:00Z"
|
||||
},
|
||||
"sensorValuesList": {},
|
||||
"avgSoilMoisture": {},
|
||||
"sensorRadarChart": {},
|
||||
"sensorComparisonChart": {},
|
||||
"anomalyDetectionCard": {},
|
||||
"soilMoistureHeatmap": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. ثبت payload خارجی دستگاه
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `POST /api/device-hub/external/`
|
||||
|
||||
### کاربرد
|
||||
|
||||
این endpoint برای سیستم یا دستگاه خارجی است تا payload را به backend ارسال کند.
|
||||
این endpoint با API key اختصاصی کار میکند، نه با توکن کاربر.
|
||||
|
||||
### Header
|
||||
|
||||
```http
|
||||
X-API-Key: <api_key>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### Body
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "22222222-2222-2222-2222-222222222222",
|
||||
"payload": {
|
||||
"moisture_percent": 32.5,
|
||||
"temperature_c": 21.3,
|
||||
"ph": 6.7,
|
||||
"ec_ds_m": 1.1,
|
||||
"nitrogen_mg_kg": 42,
|
||||
"phosphorus_mg_kg": 18,
|
||||
"potassium_mg_kg": 210
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### رفتار endpoint
|
||||
|
||||
- payload را در `SensorExternalRequestLog` ذخیره میکند
|
||||
- notification میسازد
|
||||
- payload را به farm-data service فوروارد میکند
|
||||
|
||||
### پاسخ موفق نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 201,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"title": "Sensor external API request"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### خطاهای مهم
|
||||
|
||||
- `401`: اگر `X-API-Key` اشتباه یا خالی باشد
|
||||
- `404`: اگر `physical device` پیدا نشود
|
||||
- `503`: اگر migrationها آماده نباشند یا سرویس farm-data در دسترس نباشد
|
||||
|
||||
---
|
||||
|
||||
## 12. لیست لاگهای ورودی خارجی
|
||||
|
||||
### Endpoint
|
||||
|
||||
- `GET /api/device-hub/external/logs/?farm_uuid=<farm_uuid>&page=1&page_size=20`
|
||||
|
||||
### Query Params
|
||||
|
||||
- `farm_uuid` اجباری
|
||||
- `page` اجباری در داکیومنت، ولی در عمل اگر نفرستید پیشفرض `1` دارد در paginator
|
||||
- `page_size` اجباری در داکیومنت
|
||||
- `physical_device_uuid` اختیاری
|
||||
- `sensor_type` اختیاری
|
||||
- `date_from` اختیاری
|
||||
- `date_to` اختیاری
|
||||
|
||||
### پاسخ نمونه
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"count": 25,
|
||||
"next": "https://example.com/api/device-hub/external/logs/?page=2",
|
||||
"previous": null,
|
||||
"data": [
|
||||
{
|
||||
"id": 10,
|
||||
"farm_uuid": "44444444-4444-4444-4444-444444444444",
|
||||
"sensor_catalog_uuid": "11111111-1111-1111-1111-111111111111",
|
||||
"physical_device_uuid": "22222222-2222-2222-2222-222222222222",
|
||||
"farm_device": null,
|
||||
"sensor_catalog": null,
|
||||
"payload": {
|
||||
"moisture": 52.4
|
||||
},
|
||||
"created_at": "2025-01-01T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## الگوی خطاها
|
||||
|
||||
### Validation Error
|
||||
|
||||
در بیشتر خطاهای اعتبارسنجی، پاسخ شبیه این است:
|
||||
|
||||
```json
|
||||
{
|
||||
"device_code": [
|
||||
"Device code is not attached to this farm device."
|
||||
]
|
||||
}
|
||||
```
|
||||
یا:
|
||||
|
||||
```json
|
||||
{
|
||||
"physical_device_uuid": [
|
||||
"Device not found."
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Unauthorized
|
||||
|
||||
اگر توکن کاربر یا API key درست نباشد، پاسخ 401 دریافت میکنید.
|
||||
|
||||
### Service Unavailable
|
||||
|
||||
در endpointهای `external` اگر migration یا سرویس وابسته آماده نباشد:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 503,
|
||||
"msg": "Required tables are not ready. Run migrations."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ترتیب پیشنهادی استفاده در فرانت
|
||||
|
||||
برای صفحه جزئیات device معمولاً این ترتیب مناسب است:
|
||||
|
||||
1. گرفتن catalogها از `GET /api/device-hub/catalog/`
|
||||
2. گرفتن جزئیات device از `GET /api/device-hub/devices/<uuid>/?device_code=...`
|
||||
3. گرفتن summary از `GET /api/device-hub/devices/<uuid>/summary/?device_code=...`
|
||||
4. در صورت نیاز گرفتن:
|
||||
- latest payload
|
||||
- comparison chart
|
||||
- radar chart
|
||||
- values list
|
||||
- logs
|
||||
|
||||
برای deviceهای commandable:
|
||||
|
||||
1. از `commands_schema` در catalog commandهای مجاز را بخوانید
|
||||
2. سپس به `POST /api/device-hub/devices/<uuid>/commands/` درخواست بزنید
|
||||
|
||||
---
|
||||
|
||||
## محل فایلهای مرتبط در کد
|
||||
|
||||
- مسیرها: `device_hub/urls.py`
|
||||
- ویوها: `device_hub/views.py`
|
||||
- serializerها: `device_hub/serializers.py`
|
||||
- serializerهای summary/chart: `device_hub/sensor_serializers.py`
|
||||
- مسیر پایه پروژه: `config/urls.py`
|
||||
@@ -155,6 +155,11 @@ class DeviceCommandResponseSerializer(serializers.Serializer):
|
||||
status = serializers.CharField()
|
||||
|
||||
|
||||
class DeviceCodeListResponseSerializer(serializers.Serializer):
|
||||
physical_device_uuid = serializers.UUIDField()
|
||||
device_codes = serializers.ListField(child=serializers.CharField())
|
||||
|
||||
|
||||
class SensorExternalRequestLogSerializer(serializers.ModelSerializer):
|
||||
farm_device = serializers.SerializerMethodField()
|
||||
sensor_catalog = serializers.SerializerMethodField()
|
||||
|
||||
@@ -1097,7 +1097,7 @@ def build_device_soil_moisture_heatmap(farm_device, context=None, *, device_cata
|
||||
message=f"Device heatmap has no usable numeric series for farm_device_id={getattr(farm_device, 'id', None)}.",
|
||||
details={"farm_device_id": getattr(farm_device, "id", None)},
|
||||
)
|
||||
sensor_name = farm_device.name if farm_device and farm_device.name else SOIL_MOISTURE_HEATMAP["zones"][0]
|
||||
sensor_name = farm_device.name if farm_device and farm_device.name else "Sensor"
|
||||
return {
|
||||
"zones": [sensor_name],
|
||||
"hours": [point["x"] for point in chart_points],
|
||||
@@ -1128,7 +1128,7 @@ def build_device_avg_primary_metric(farm_device, context=None, *, device_catalog
|
||||
)
|
||||
chip_text, chip_color, avatar_color = _calculate_status_chip(primary_value)
|
||||
return {
|
||||
**deepcopy(AVG_SOIL_MOISTURE),
|
||||
**deepcopy(AVG_SOIL_MOISTURE_TEMPLATE),
|
||||
"title": primary_field["label"],
|
||||
"stats": _format_value(primary_value, primary_field["unit"]),
|
||||
"chipText": chip_text,
|
||||
|
||||
+31
-1
@@ -6,7 +6,7 @@ from farm_hub.models import FarmHub, FarmType
|
||||
|
||||
from .models import DeviceCatalog, SensorExternalRequestLog
|
||||
from .services import DeviceDataUnavailableError, build_device_anomaly_detection_card
|
||||
from .views import DeviceCommandView, DeviceDetailView, DeviceLatestPayloadView, DeviceSummaryView
|
||||
from .views import DeviceCodeListView, DeviceCommandView, DeviceDetailView, DeviceLatestPayloadView, DeviceSummaryView
|
||||
|
||||
|
||||
class DeviceHubGenericViewsTests(TestCase):
|
||||
@@ -66,6 +66,36 @@ class DeviceHubGenericViewsTests(TestCase):
|
||||
self.assertEqual(response.data["data"]["physical_device_uuid"], str(self.device.physical_device_uuid))
|
||||
self.assertEqual(response.data["data"]["device_catalog"]["code"], self.catalog.code)
|
||||
|
||||
def test_device_code_list_view_returns_attached_device_codes(self):
|
||||
secondary_catalog = DeviceCatalog.objects.create(
|
||||
code="air_sensor_v1",
|
||||
name="Air Sensor V1",
|
||||
device_communication_type=DeviceCatalog.OUTPUT_ONLY,
|
||||
)
|
||||
self.device.device_catalogs.add(self.catalog, secondary_catalog)
|
||||
|
||||
request = self.factory.get(
|
||||
f"/api/device-hub/devices/{self.device.physical_device_uuid}/device-codes/",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = DeviceCodeListView.as_view()(request, physical_device_uuid=self.device.physical_device_uuid)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["data"]["physical_device_uuid"], str(self.device.physical_device_uuid))
|
||||
self.assertEqual(response.data["data"]["device_codes"], [self.catalog.code, secondary_catalog.code])
|
||||
|
||||
def test_device_code_list_view_returns_primary_catalog_when_no_m2m_catalogs_exist(self):
|
||||
request = self.factory.get(
|
||||
f"/api/device-hub/devices/{self.device.physical_device_uuid}/device-codes/",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = DeviceCodeListView.as_view()(request, physical_device_uuid=self.device.physical_device_uuid)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["data"]["device_codes"], [self.catalog.code])
|
||||
|
||||
def test_device_latest_payload_view_returns_normalized_readings(self):
|
||||
request = self.factory.get(
|
||||
f"/api/device-hub/devices/{self.device.physical_device_uuid}/latest/",
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import DeviceCatalogListView, DeviceCommandView, DeviceComparisonChartView, DeviceDetailView, DeviceLatestPayloadView, DeviceLogListView, DeviceRadarChartView, DeviceSummaryView, DeviceValuesListView, Sensor7In1SummaryView, SensorExternalAPIView, SensorExternalRequestLogListAPIView
|
||||
from .views import DeviceCatalogListView, DeviceCodeListView, DeviceCommandView, DeviceComparisonChartView, DeviceDetailView, DeviceLatestPayloadView, DeviceLogListView, DeviceRadarChartView, DeviceSummaryView, DeviceValuesListView, Sensor7In1SummaryView, SensorExternalAPIView, SensorExternalRequestLogListAPIView
|
||||
|
||||
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"),
|
||||
|
||||
+19
-1
@@ -12,7 +12,7 @@ from soil.serializers import SoilComparisonChartSerializer, SoilRadarChartSerial
|
||||
|
||||
from .authentication import SensorExternalAPIKeyAuthentication
|
||||
from .sensor_serializers import DeviceSummarySerializer, Sensor7In1SummarySerializer, SensorComparisonChartQuerySerializer, SensorComparisonChartResponseSerializer, SensorRadarChartQuerySerializer, SensorRadarChartResponseSerializer, SensorValuesListQuerySerializer, SensorValuesListResponseSerializer
|
||||
from .serializers import DeviceCatalogSerializer, DeviceCodeQuerySerializer, DeviceCommandRequestSerializer, DeviceCommandResponseSerializer, DeviceDetailSerializer, DeviceLatestPayloadSerializer, DeviceRangeQuerySerializer, SensorExternalRequestLogQuerySerializer, SensorExternalRequestLogSerializer, SensorExternalRequestSerializer
|
||||
from .serializers import DeviceCatalogSerializer, DeviceCodeListResponseSerializer, DeviceCodeQuerySerializer, DeviceCommandRequestSerializer, DeviceCommandResponseSerializer, DeviceDetailSerializer, DeviceLatestPayloadSerializer, DeviceRangeQuerySerializer, SensorExternalRequestLogQuerySerializer, SensorExternalRequestLogSerializer, SensorExternalRequestSerializer
|
||||
from .services import DeviceDataUnavailableError, FarmDataForwardError, build_device_comparison_chart, build_device_latest_payload, build_device_radar_chart, build_device_summary, build_device_values_list, create_sensor_external_notification, execute_device_command, forward_sensor_payload_to_farm_data, get_farm_device_by_physical_uuid, get_farm_device_map_for_logs, get_primary_soil_sensor, get_sensor_7_in_1_comparison_chart_data, get_sensor_7_in_1_radar_chart_data, get_sensor_7_in_1_summary_data, get_sensor_comparison_chart_data, get_sensor_external_request_logs_for_farm, get_sensor_radar_chart_data, get_sensor_values_list_data, validate_output_device_catalog
|
||||
|
||||
|
||||
@@ -51,6 +51,24 @@ class DeviceDetailView(DeviceBaseView):
|
||||
return Response({"code": 200, "msg": "success", "data": serializer.data}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DeviceCodeListView(DeviceBaseView):
|
||||
@extend_schema(tags=["Device Hub"], responses={200: code_response("DeviceCodeListResponse", data=DeviceCodeListResponseSerializer())})
|
||||
def get(self, request, physical_device_uuid):
|
||||
farm_device = self.get_farm_device(request, physical_device_uuid)
|
||||
device_codes = [catalog.code for catalog in farm_device.get_device_catalogs() if getattr(catalog, "code", "")]
|
||||
return Response(
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"physical_device_uuid": farm_device.physical_device_uuid,
|
||||
"device_codes": device_codes,
|
||||
},
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class DeviceLatestPayloadView(DeviceBaseView):
|
||||
@extend_schema(tags=["Device Hub"], parameters=[OpenApiParameter(name="device_code", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=True)], responses={200: code_response("DeviceLatestPayloadResponse", data=DeviceLatestPayloadSerializer())})
|
||||
def get(self, request, physical_device_uuid):
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user