UPDATE
This commit is contained in:
@@ -0,0 +1,860 @@
|
||||
# راهنمای طراحی Device Catalog داینامیک
|
||||
|
||||
## هدف
|
||||
|
||||
هدف این تغییر این است که اضافه کردن یک دیوایس جدید فقط با ثبت اطلاعات در دیتابیس یا پنل ادمین انجام شود و برای هر دیوایس جدید نیازی به اضافه کردن فایل، ویو، serializer یا service جدید در کد نباشد.
|
||||
|
||||
الان ساختار پروژه برای بعضی دیوایسها device-specific است؛ مثلا:
|
||||
|
||||
- `device_hub/sensor_7_in_1_urls.py`
|
||||
- `Sensor7In1SummaryView`
|
||||
- `get_sensor_7_in_1_summary_data`
|
||||
- `get_sensor_7_in_1_radar_chart_data`
|
||||
- `get_sensor_7_in_1_comparison_chart_data`
|
||||
|
||||
این ساختار برای یک MVP خوب است، ولی برای scale شدن مناسب نیست. چون برای هر دیوایس جدید باید:
|
||||
|
||||
- route جدید بسازید
|
||||
- view جدید بسازید
|
||||
- serializer جدید بسازید
|
||||
- service جدید بسازید
|
||||
- منطق mapping payload جدید اضافه کنید
|
||||
|
||||
این دقیقا چیزی است که باید حذف شود.
|
||||
|
||||
---
|
||||
|
||||
## مشکل ساختار فعلی
|
||||
|
||||
الان backend تا حدی بر اساس `device type` یا `sensor-7-in-1` branch میزند، نه بر اساس یک configuration عمومی.
|
||||
|
||||
نمونهها:
|
||||
|
||||
- `device_hub/views.py`
|
||||
- `Sensor7In1SummaryView`
|
||||
- `Sensor7In1RadarChartView`
|
||||
- `Sensor7In1ComparisonChartView`
|
||||
- `device_hub/services.py`
|
||||
- `get_primary_soil_sensor`
|
||||
- `get_sensor_7_in_1_summary_data`
|
||||
- `get_sensor_7_in_1_values_list_data`
|
||||
- `get_sensor_7_in_1_radar_chart_data`
|
||||
- `get_sensor_7_in_1_comparison_chart_data`
|
||||
- `device_hub/sensor_serializers.py`
|
||||
- `Sensor7In1SummarySerializer`
|
||||
- `Sensor7In1MetaSerializer`
|
||||
|
||||
مشکل این approach:
|
||||
|
||||
1. اضافه شدن هر device جدید نیاز به deploy کد دارد.
|
||||
2. naming پروژه به device خاص وابسته میشود.
|
||||
3. APIها generic نیستند.
|
||||
4. frontend مجبور میشود endpointهای مخصوص هر device را صدا بزند.
|
||||
5. منطق business بهجای data-driven بودن، hard-coded شده است.
|
||||
|
||||
---
|
||||
|
||||
## معماری پیشنهادی
|
||||
|
||||
### اصل طراحی
|
||||
|
||||
بهجای اینکه برای هر device endpoint جدا داشته باشیم، باید فقط یک سری endpoint عمومی داشته باشیم که بر اساس:
|
||||
|
||||
- `physical_device_uuid`
|
||||
یا
|
||||
- `device_catalog_uuid`
|
||||
یا
|
||||
- `device_catalog.code`
|
||||
|
||||
اطلاعات همان device را برگردانند.
|
||||
|
||||
یعنی backend باید:
|
||||
|
||||
1. device را پیدا کند
|
||||
2. configuration آن device را از catalog بخواند
|
||||
3. payload mapping آن device را بخواند
|
||||
4. widgetهای قابل نمایش آن را تشخیص دهد
|
||||
5. خروجی استاندارد بسازد
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
GET /api/device-hub/catalog/
|
||||
```
|
||||
|
||||
کاربرد:
|
||||
|
||||
- لیست همه device catalogها
|
||||
- metadata هر catalog
|
||||
- نوع ارتباط device
|
||||
- فیلدهای قابل نمایش
|
||||
|
||||
---
|
||||
|
||||
### 2) جزئیات یک دیوایس ثبتشده روی مزرعه
|
||||
|
||||
```http
|
||||
GET /api/device-hub/devices/{physical_device_uuid}/?device_code=soil_sensor_v1
|
||||
```
|
||||
|
||||
نکته:
|
||||
|
||||
- در این endpoint، `device_code` باید در query string ارسال شود.
|
||||
- اگر device فقط یک catalog داشته باشد، از نظر معماری باز هم بهتر است frontend آن را صریح بفرستد.
|
||||
|
||||
پاسخ نمونه:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"uuid": "farm-device-uuid",
|
||||
"physical_device_uuid": "device-uuid",
|
||||
"name": "Soil Sensor #1",
|
||||
"device_catalog": {
|
||||
"uuid": "catalog-uuid",
|
||||
"code": "soil_sensor_v1",
|
||||
"name": "Soil Sensor V1",
|
||||
"device_communication_type": "output_only"
|
||||
},
|
||||
"specifications": {},
|
||||
"power_source": {},
|
||||
"last_payload_at": "2025-01-01T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3) آخرین دادهی یک device
|
||||
|
||||
```http
|
||||
GET /api/device-hub/devices/{physical_device_uuid}/latest/?device_code=soil_sensor_v1
|
||||
```
|
||||
|
||||
کاربرد:
|
||||
|
||||
- آخرین payload خام
|
||||
- آخرین payload نرمالشده
|
||||
- آخرین readingهای قابل نمایش
|
||||
|
||||
---
|
||||
|
||||
### 4) summary داینامیک برای یک device
|
||||
|
||||
```http
|
||||
GET /api/device-hub/devices/{physical_device_uuid}/summary/?device_code=soil_sensor_v1
|
||||
```
|
||||
|
||||
کاربرد:
|
||||
|
||||
- بهجای `sensor_7_in_1/summary`
|
||||
- خروجی بر اساس config همان device
|
||||
|
||||
---
|
||||
|
||||
### 5) نمودار مقایسهای داینامیک
|
||||
|
||||
```http
|
||||
GET /api/device-hub/devices/{physical_device_uuid}/comparison-chart/?device_code=soil_sensor_v1&range=7d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6) نمودار رادار داینامیک
|
||||
|
||||
```http
|
||||
GET /api/device-hub/devices/{physical_device_uuid}/radar-chart/?device_code=soil_sensor_v1&range=7d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7) values list داینامیک
|
||||
|
||||
```http
|
||||
GET /api/device-hub/devices/{physical_device_uuid}/values-list/?device_code=soil_sensor_v1&range=7d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8) دریافت history خام
|
||||
|
||||
```http
|
||||
GET /api/device-hub/devices/{physical_device_uuid}/logs/?device_code=soil_sensor_v1&page=1&page_size=20
|
||||
```
|
||||
|
||||
این endpoint برای debug و audit خیلی مهم است.
|
||||
|
||||
---
|
||||
|
||||
## تغییر مهم در مدلها
|
||||
|
||||
### 1) `DeviceCatalog`
|
||||
|
||||
الان این مدل شروع خوبی دارد، ولی برای dynamic شدن کافی نیست.
|
||||
|
||||
مدل فعلی در:
|
||||
|
||||
- `device_hub/models.py:6`
|
||||
|
||||
فیلدهای پیشنهادی جدید:
|
||||
|
||||
```python
|
||||
display_schema = models.JSONField(default=dict, blank=True)
|
||||
payload_mapping = models.JSONField(default=dict, blank=True)
|
||||
supported_widgets = models.JSONField(default=list, blank=True)
|
||||
commands_schema = models.JSONField(default=list, blank=True)
|
||||
capabilities = models.JSONField(default=list, blank=True)
|
||||
```
|
||||
|
||||
### توضیح هر فیلد
|
||||
|
||||
#### `payload_mapping`
|
||||
|
||||
مشخص میکند payload خام این device چطور به فیلدهای استاندارد سیستم map شود.
|
||||
|
||||
مثال:
|
||||
|
||||
```json
|
||||
{
|
||||
"soil_moisture": ["soil_moisture", "soilMoisture", "moisture"],
|
||||
"soil_temperature": ["soil_temperature", "soilTemperature", "temperature"],
|
||||
"soil_ph": ["soil_ph", "soilPh", "ph"]
|
||||
}
|
||||
```
|
||||
|
||||
#### `display_schema`
|
||||
|
||||
مشخص میکند کدام فیلدها در UI نمایش داده شوند و label و unit آنها چیست.
|
||||
|
||||
مثال:
|
||||
|
||||
```json
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"id": "soil_moisture",
|
||||
"label": "رطوبت خاک",
|
||||
"unit": "%",
|
||||
"ideal_min": 45,
|
||||
"ideal_max": 65
|
||||
},
|
||||
{
|
||||
"id": "soil_temperature",
|
||||
"label": "دمای خاک",
|
||||
"unit": "°C",
|
||||
"ideal_min": 18,
|
||||
"ideal_max": 28
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `supported_widgets`
|
||||
|
||||
مشخص میکند برای این device چه widgetهایی فعال باشند.
|
||||
|
||||
مثال:
|
||||
|
||||
```json
|
||||
[
|
||||
"values_list",
|
||||
"comparison_chart",
|
||||
"radar_chart",
|
||||
"latest_payload",
|
||||
"anomaly_card"
|
||||
]
|
||||
```
|
||||
|
||||
#### `commands_schema`
|
||||
|
||||
برای deviceهایی که `input_only` هستند.
|
||||
|
||||
مثال:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"command": "turn_on",
|
||||
"label": "روشن کردن",
|
||||
"payload_schema": {
|
||||
"duration_seconds": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "turn_off",
|
||||
"label": "خاموش کردن",
|
||||
"payload_schema": {}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### `capabilities`
|
||||
|
||||
فهرست capabilityهای device:
|
||||
|
||||
```json
|
||||
["measure", "history", "alert", "command"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## برای deviceهای ورودیمحور
|
||||
|
||||
شما گفتی بعضی deviceها فقط باید دستور بگیرند و خروجی نمیدهند. این دقیقا باید در مدل و API مشخص باشد.
|
||||
|
||||
برای این نوع device:
|
||||
|
||||
- `device_communication_type = "input_only"`
|
||||
- `returned_data_fields = []`
|
||||
- `supported_widgets = []`
|
||||
- `commands_schema` باید پر باشد
|
||||
|
||||
API پیشنهادی:
|
||||
|
||||
```http
|
||||
POST /api/device-hub/devices/{physical_device_uuid}/commands/
|
||||
```
|
||||
|
||||
payload نمونه:
|
||||
|
||||
```json
|
||||
{
|
||||
"device_code": "irrigation_valve_v1",
|
||||
"command": "turn_on",
|
||||
"payload": {
|
||||
"duration_seconds": 120
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
پاسخ نمونه:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "command accepted",
|
||||
"data": {
|
||||
"physical_device_uuid": "device-uuid",
|
||||
"command": "turn_on",
|
||||
"status": "queued"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## چه جاهایی باید در پروژه تغییر کند
|
||||
|
||||
### 1) حذف وابستگی به `sensor_7_in_1`
|
||||
|
||||
#### فایلهایی که باید refactor شوند
|
||||
|
||||
- `device_hub/views.py`
|
||||
- `device_hub/services.py`
|
||||
- `device_hub/sensor_serializers.py`
|
||||
- `device_hub/sensor_7_in_1_urls.py`
|
||||
- `device_hub/comparison_urls.py`
|
||||
- `device_hub/urls.py`
|
||||
|
||||
#### چه چیزی باید تغییر کند
|
||||
|
||||
- viewهای device-specific حذف شوند
|
||||
- routeهای generic جایگزین شوند
|
||||
- serviceهای `get_sensor_7_in_1_*` به serviceهای generic تبدیل شوند
|
||||
|
||||
---
|
||||
|
||||
### 2) ساخت service عمومی برای پیدا کردن device
|
||||
|
||||
در `device_hub/services.py` باید این لایهها ایجاد شود:
|
||||
|
||||
#### الف) resolver
|
||||
|
||||
```python
|
||||
get_farm_device_by_physical_uuid(physical_device_uuid)
|
||||
get_device_catalog_for_farm_device(farm_device)
|
||||
get_latest_device_log(farm_device)
|
||||
get_device_logs(farm_device, range_value=None)
|
||||
```
|
||||
|
||||
#### ب) normalizer
|
||||
|
||||
```python
|
||||
normalize_device_payload(device_catalog, payload)
|
||||
extract_device_readings(device_catalog, payload)
|
||||
```
|
||||
|
||||
این بخش باید از `payload_mapping` استفاده کند، نه از `SENSOR_FIELDS` ثابت.
|
||||
|
||||
#### ج) presenter / builder
|
||||
|
||||
```python
|
||||
build_device_summary(farm_device)
|
||||
build_device_values_list(farm_device, range_value)
|
||||
build_device_comparison_chart(farm_device, range_value)
|
||||
build_device_radar_chart(farm_device, range_value)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3) ثابتهای hard-coded باید از کد خارج شوند
|
||||
|
||||
الان این موارد hard-coded هستند:
|
||||
|
||||
- `SENSOR_FIELDS`
|
||||
- `COMPARISON_CHART_FIELD_ALIASES`
|
||||
- `VALUES_LIST_FIELDS`
|
||||
- `RADAR_CHART_FIELDS`
|
||||
|
||||
اینها الان در:
|
||||
|
||||
- `device_hub/services.py:16`
|
||||
|
||||
هستند و باید به config وابسته به `DeviceCatalog` منتقل شوند.
|
||||
|
||||
یعنی:
|
||||
|
||||
- بهجای constant سراسری
|
||||
- از `device_catalog.display_schema`
|
||||
- و `device_catalog.payload_mapping`
|
||||
|
||||
استفاده شود.
|
||||
|
||||
---
|
||||
|
||||
### 4) serializerهای اختصاصی باید generic شوند
|
||||
|
||||
الان در:
|
||||
|
||||
- `device_hub/sensor_serializers.py:6`
|
||||
|
||||
serializerها مخصوص 7-in-1 هستند.
|
||||
|
||||
باید اینها جایگزین شوند:
|
||||
|
||||
- `DeviceMetaSerializer`
|
||||
- `DeviceFieldValueSerializer`
|
||||
- `DeviceValuesListSerializer`
|
||||
- `DeviceSummarySerializer`
|
||||
- `DeviceComparisonChartSerializer`
|
||||
- `DeviceRadarChartSerializer`
|
||||
|
||||
یعنی نام serializer نباید به یک device خاص گره خورده باشد.
|
||||
|
||||
---
|
||||
|
||||
### 5) endpointهای generic بسازید
|
||||
|
||||
در `device_hub/urls.py` بهتر است چیزی شبیه این داشته باشید:
|
||||
|
||||
```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"),
|
||||
path("devices/<uuid:physical_device_uuid>/values-list/", DeviceValuesListView.as_view(), name="device-values-list"),
|
||||
path("devices/<uuid:physical_device_uuid>/comparison-chart/", DeviceComparisonChartView.as_view(), name="device-comparison-chart"),
|
||||
path("devices/<uuid:physical_device_uuid>/radar-chart/", DeviceRadarChartView.as_view(), name="device-radar-chart"),
|
||||
path("devices/<uuid:physical_device_uuid>/logs/", DeviceLogListView.as_view(), name="device-log-list"),
|
||||
path("devices/<uuid:physical_device_uuid>/commands/", DeviceCommandView.as_view(), name="device-command"),
|
||||
path("external/", SensorExternalAPIView.as_view(), name="sensor-external-api"),
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## روند اضافه کردن device جدید بدون تغییر کد
|
||||
|
||||
بعد از این refactor، اضافه کردن device جدید باید اینطوری باشد:
|
||||
|
||||
### مرحله 1
|
||||
|
||||
یک رکورد جدید در `DeviceCatalog` ایجاد شود.
|
||||
|
||||
### مرحله 2
|
||||
|
||||
این اطلاعات برایش ثبت شود:
|
||||
|
||||
- `code`
|
||||
- `name`
|
||||
- `device_communication_type`
|
||||
- `payload_mapping`
|
||||
- `display_schema`
|
||||
- `supported_widgets`
|
||||
- `commands_schema`
|
||||
|
||||
### مرحله 3
|
||||
|
||||
هنگام ثبت `FarmDevice`، آن device به همین catalog وصل شود.
|
||||
|
||||
### مرحله 4
|
||||
|
||||
از این به بعد frontend فقط با `physical_device_uuid` به endpointهای generic میزند.
|
||||
|
||||
بدون تغییر کد.
|
||||
|
||||
---
|
||||
|
||||
## نمونه config برای یک سنسور خروجیمحور
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "soil_sensor_v2",
|
||||
"name": "Soil Sensor V2",
|
||||
"device_communication_type": "output_only",
|
||||
"returned_data_fields": [
|
||||
"soil_moisture",
|
||||
"soil_temperature",
|
||||
"soil_ph"
|
||||
],
|
||||
"payload_mapping": {
|
||||
"soil_moisture": ["moisture", "soil_moisture"],
|
||||
"soil_temperature": ["temperature", "soil_temperature"],
|
||||
"soil_ph": ["ph", "soil_ph"]
|
||||
},
|
||||
"display_schema": {
|
||||
"fields": [
|
||||
{
|
||||
"id": "soil_moisture",
|
||||
"label": "رطوبت خاک",
|
||||
"unit": "%",
|
||||
"ideal_min": 45,
|
||||
"ideal_max": 65
|
||||
},
|
||||
{
|
||||
"id": "soil_temperature",
|
||||
"label": "دمای خاک",
|
||||
"unit": "°C",
|
||||
"ideal_min": 18,
|
||||
"ideal_max": 28
|
||||
},
|
||||
{
|
||||
"id": "soil_ph",
|
||||
"label": "PH خاک",
|
||||
"unit": "pH",
|
||||
"ideal_min": 6,
|
||||
"ideal_max": 7.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"supported_widgets": [
|
||||
"values_list",
|
||||
"comparison_chart",
|
||||
"radar_chart",
|
||||
"latest_payload"
|
||||
],
|
||||
"commands_schema": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## نمونه config برای یک device فقط ورودی
|
||||
|
||||
مثلا شیر برقی یا پمپ:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "irrigation_valve_v1",
|
||||
"name": "Irrigation Valve V1",
|
||||
"device_communication_type": "input_only",
|
||||
"returned_data_fields": [],
|
||||
"payload_mapping": {},
|
||||
"display_schema": {
|
||||
"fields": []
|
||||
},
|
||||
"supported_widgets": [],
|
||||
"commands_schema": [
|
||||
{
|
||||
"command": "open",
|
||||
"label": "باز کردن شیر",
|
||||
"payload_schema": {
|
||||
"duration_seconds": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "close",
|
||||
"label": "بستن شیر",
|
||||
"payload_schema": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## پیشنهاد مرحلهبندی پیادهسازی
|
||||
|
||||
### فاز 1: Generic read API
|
||||
|
||||
اول اینها را بسازید:
|
||||
|
||||
- `DeviceDetailView`
|
||||
- `DeviceLatestPayloadView`
|
||||
- `DeviceSummaryView`
|
||||
- `DeviceValuesListView`
|
||||
- `DeviceComparisonChartView`
|
||||
- `DeviceRadarChartView`
|
||||
|
||||
و فعلا داده را با fallback از منطق فعلی بسازید.
|
||||
|
||||
### فاز 2: Config-driven normalization
|
||||
|
||||
بعد:
|
||||
|
||||
- `payload_mapping`
|
||||
- `display_schema`
|
||||
- `supported_widgets`
|
||||
|
||||
را به `DeviceCatalog` اضافه کنید و منطق hard-coded را حذف کنید.
|
||||
|
||||
### فاز 3: Command API
|
||||
|
||||
برای `input_only` deviceها:
|
||||
|
||||
- `DeviceCommandView`
|
||||
- command validation
|
||||
- queue / external broker integration
|
||||
|
||||
### فاز 4: Admin / CMS support
|
||||
|
||||
برای اینکه بدون کد device جدید اضافه شود، باید از طریق:
|
||||
|
||||
- Django Admin
|
||||
یا
|
||||
- پنل داخلی
|
||||
|
||||
بتوانید `DeviceCatalog` را مدیریت کنید.
|
||||
|
||||
---
|
||||
|
||||
## حداقل تغییرهایی که همین الان باید انجام بدهید
|
||||
|
||||
اگر بخواهی با کمترین تغییر از ساختار فعلی به ساختار بهتر برسی، اینها مهمترین کارها هستند:
|
||||
|
||||
### ضروری
|
||||
|
||||
1. حذف endpointهای `sensor_7_in_1`-محور
|
||||
2. ساخت endpointهای generic با `physical_device_uuid`
|
||||
3. جدا کردن منطق extraction از device-specific code
|
||||
4. انتقال field mapping از constant به دیتابیس
|
||||
5. اضافه کردن schema برای commandها
|
||||
|
||||
### مهم ولی فاز بعدی
|
||||
|
||||
1. admin برای `DeviceCatalog`
|
||||
2. validation قوی برای `payload_mapping`
|
||||
3. caching برای summary/chartها
|
||||
4. swagger dynamic docs برای command schema
|
||||
|
||||
---
|
||||
|
||||
## جمعبندی
|
||||
|
||||
اگر هدفت این است که:
|
||||
|
||||
- device جدید بدون تغییر کد اضافه شود
|
||||
- frontend فقط با `device_uuid` کار کند
|
||||
- بعضی deviceها فقط command بگیرند
|
||||
- بعضی deviceها telemetry بدهند
|
||||
|
||||
پس باید طراحی از:
|
||||
|
||||
- `device-specific code`
|
||||
|
||||
به این مدل تغییر کند:
|
||||
|
||||
- `catalog-driven architecture`
|
||||
|
||||
یعنی:
|
||||
|
||||
- `DeviceCatalog` منبع حقیقت باشد
|
||||
- APIها generic باشند
|
||||
- parsing و rendering بر اساس config انجام شود
|
||||
- commandها هم از schema خود device خوانده شوند
|
||||
|
||||
---
|
||||
|
||||
## فایلهای کلیدی برای refactor
|
||||
|
||||
- `device_hub/models.py:6`
|
||||
- `device_hub/views.py:19`
|
||||
- `device_hub/services.py:16`
|
||||
- `device_hub/sensor_serializers.py:1`
|
||||
- `device_hub/urls.py:1`
|
||||
- `device_hub/sensor_7_in_1_urls.py:1`
|
||||
- `device_hub/comparison_urls.py:1`
|
||||
- `device_hub/seeds.py:12`
|
||||
|
||||
---
|
||||
|
||||
## پیشنهاد نهایی
|
||||
|
||||
بهترین مسیر این است که:
|
||||
|
||||
1. endpointهای generic را اضافه کنی
|
||||
2. endpointهای قدیمی `sensor_7_in_1` را deprecated کنی
|
||||
3. config مورد نیاز را به `DeviceCatalog` اضافه کنی
|
||||
4. frontend را به `physical_device_uuid`-based API منتقل کنی
|
||||
|
||||
اگر خواستی، در مرحله بعد من میتوانم همین طراحی را به تسک اجرایی تبدیل کنم و دقیقا بگویم:
|
||||
|
||||
- چه model fieldهایی اضافه شوند
|
||||
- چه serializerهایی ساخته شوند
|
||||
- چه endpointهایی پیاده شوند
|
||||
- و refactor را در چه ترتیب انجام بدهی
|
||||
Reference in New Issue
Block a user