diff --git a/celerybeat-schedule b/celerybeat-schedule index fcb7c4f..74ffe11 100644 Binary files a/celerybeat-schedule and b/celerybeat-schedule differ diff --git a/dashboard/services.py b/dashboard/services.py index feaf44c..6e35277 100644 --- a/dashboard/services.py +++ b/dashboard/services.py @@ -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} diff --git a/dashboard/templates.py b/dashboard/templates.py index ab4df50..0fcd02e 100644 --- a/dashboard/templates.py +++ b/dashboard/templates.py @@ -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", }, ] diff --git a/dashboard/tests.py b/dashboard/tests.py index fbb5e64..6669faf 100644 --- a/dashboard/tests.py +++ b/dashboard/tests.py @@ -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/") diff --git a/device_hub/API_GUIDE.md b/device_hub/API_GUIDE.md new file mode 100644 index 0000000..4a9f3b3 --- /dev/null +++ b/device_hub/API_GUIDE.md @@ -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 +Content-Type: application/json +``` + +### 2) آدرس پایه + +اگر دامنه پروژه مثلاً `https://example.com` باشد، آدرس کامل endpointها به این صورت است: + +- `https://example.com/api/device-hub/catalog/` +- `https://example.com/api/device-hub/devices//latest/?device_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 " +``` + +### پاسخ نمونه + +```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//?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 " +``` + +### پاسخ نمونه + +```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//latest/?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//summary/?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//comparison-chart/?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//values-list/?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//radar-chart/?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//logs/?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//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 " \ + -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=` + +### کاربرد + +خلاصه‌ای از سنسور اصلی مزرعه برای 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: +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=&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//?device_code=...` +3. گرفتن summary از `GET /api/device-hub/devices//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//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` diff --git a/device_hub/serializers.py b/device_hub/serializers.py index a562f20..3799a7d 100644 --- a/device_hub/serializers.py +++ b/device_hub/serializers.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() diff --git a/device_hub/services.py b/device_hub/services.py index 9576fb1..9308969 100644 --- a/device_hub/services.py +++ b/device_hub/services.py @@ -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, diff --git a/device_hub/tests.py b/device_hub/tests.py index a3772ea..78d13a0 100644 --- a/device_hub/tests.py +++ b/device_hub/tests.py @@ -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/", diff --git a/device_hub/urls.py b/device_hub/urls.py index dc43f93..8d98c4d 100644 --- a/device_hub/urls.py +++ b/device_hub/urls.py @@ -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//device-codes/", DeviceCodeListView.as_view(), name="device-code-list"), path("devices//", DeviceDetailView.as_view(), name="device-detail"), path("devices//latest/", DeviceLatestPayloadView.as_view(), name="device-latest-payload"), path("devices//summary/", DeviceSummaryView.as_view(), name="device-summary"), diff --git a/device_hub/views.py b/device_hub/views.py index 6dc58e8..f2e7bc0 100644 --- a/device_hub/views.py +++ b/device_hub/views.py @@ -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): diff --git a/docs/dashboard_api_reference.md b/docs/dashboard_api_reference.md new file mode 100644 index 0000000..2a0934d --- /dev/null +++ b/docs/dashboard_api_reference.md @@ -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` + diff --git a/docs/device_catalog_dynamic_architecture.md b/docs/device_catalog_dynamic_architecture.md index cad0068..3f26430 100644 --- a/docs/device_catalog_dynamic_architecture.md +++ b/docs/device_catalog_dynamic_architecture.md @@ -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= +``` + +فیلدهای زیر برمی‌گردند: + +- `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//device-codes/", DeviceCodeListView.as_view(), name="device-code-list"), path("devices//", DeviceDetailView.as_view(), name="device-detail"), path("devices//latest/", DeviceLatestPayloadView.as_view(), name="device-latest-payload"), path("devices//summary/", DeviceSummaryView.as_view(), name="device-summary"),