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
Binary file not shown.
+54
View File
@@ -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
View File
@@ -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",
},
]
+6
View File
@@ -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/")
+734
View File
@@ -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`
+5
View File
@@ -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()
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+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"),