Files
Backend/sensor_external_api/SensorExternalAPIView.md
T

357 lines
11 KiB
Markdown
Raw Normal View History

2026-04-29 01:27:16 +03:30
# مستند API دریافت داده سنسور خارجی
این فایل رفتار endpoint زیر را توضیح می‌دهد:
`POST /api/sensor-external-api/`
این API برای دریافت payload از یک سنسور فیزیکی، ثبت آن داخل دیتابیس، ساخت نوتیفیکیشن برای مزرعه، و سپس ارسال همان داده به سرویس AI/Farm Data استفاده می‌شود.
## هدف API
این endpoint وقتی صدا زده می‌شود که یک سنسور خارجی داده جدیدی تولید کرده باشد. بک‌اند در این مسیر چند کار پشت سر هم انجام می‌دهد:
1. اعتبارسنجی API key
2. اعتبارسنجی `uuid` و `payload`
3. پیدا کردن سنسور بر اساس `physical_device_uuid`
4. ذخیره لاگ درخواست در جدول `sensor_external_request_logs`
5. ساخت notification برای مزرعه
6. ارسال داده به سرویس AI در endpoint مربوط به farm data
## مسیر و View
این endpoint در فایل `sensor_external_api/urls.py` ثبت شده است:
```python
path("", SensorExternalAPIView.as_view(), name="sensor-external-api")
```
پیاده‌سازی view در فایل `sensor_external_api/views.py` قرار دارد:
```python
class SensorExternalAPIView(APIView):
authentication_classes = [SensorExternalAPIKeyAuthentication]
permission_classes = [AllowAny]
```
## احراز هویت
این API از هدر `X-API-Key` استفاده می‌کند.
کلاس احراز هویت:
`sensor_external_api/authentication.py`
رفتار آن:
- اگر `X-API-Key` یا `Authorization` ارسال نشود، پاسخ `401` می‌دهد.
- اگر مقدار کلید اشتباه باشد، پاسخ `401` می‌دهد.
- مقدار مورد انتظار از `SENSOR_EXTERNAL_API_KEY` خوانده می‌شود.
## ورودی درخواست
serializer ورودی در فایل `sensor_external_api/serializers.py` تعریف شده است:
```python
class SensorExternalRequestSerializer(serializers.Serializer):
uuid = serializers.UUIDField()
payload = serializers.JSONField(required=False, default=dict)
```
### بدنه نمونه درخواست
```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
}
}
```
نکته:
- `uuid` در این API همان `physical_device_uuid` سنسور است.
- `payload` به همان شکلی که از سنسور می‌آید ذخیره و forward می‌شود.
## روند اجرای API
### 1) اعتبارسنجی request
در متد `post` ابتدا داده ورودی validate می‌شود:
```python
serializer = SensorExternalRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
```
اگر `uuid` معتبر نباشد یا ساختار body خراب باشد، DRF خطای `400` برمی‌گرداند.
### 2) ثبت لاگ و ساخت نوتیفیکیشن
سپس این سرویس صدا زده می‌شود:
```python
notification = create_sensor_external_notification(
physical_device_uuid=serializer.validated_data["uuid"],
payload=serializer.validated_data.get("payload"),
)
```
این تابع در فایل `sensor_external_api/services.py` قرار دارد.
کارهایی که انجام می‌دهد:
- سنسور را از جدول `FarmSensor` با `physical_device_uuid` پیدا می‌کند.
- اگر سنسور پیدا نشود، `ValueError("Physical device not found.")` می‌دهد.
- یک رکورد در جدول `sensor_external_request_logs` می‌سازد.
- یک notification برای مزرعه می‌سازد.
### رکوردی که در دیتابیس ذخیره می‌شود
مدل ذخیره‌سازی:
`sensor_external_api/models.py`
```python
class SensorExternalRequestLog(models.Model):
farm_uuid = models.UUIDField(db_index=True)
sensor_catalog_uuid = models.UUIDField(null=True, blank=True, db_index=True)
physical_device_uuid = models.UUIDField(db_index=True)
payload = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
```
یعنی payload خام سنسور برای گزارش‌گیری و استفاده‌های بعدی نگه داشته می‌شود.
### 3) ارسال داده به سرویس AI / Farm Data
بعد از ثبت لاگ، این سرویس صدا زده می‌شود:
```python
forward_sensor_payload_to_farm_data(
physical_device_uuid=serializer.validated_data["uuid"],
payload=serializer.validated_data.get("payload"),
)
```
این قسمت مهم‌ترین call خارجی endpoint است.
## این API چه آدرسی از AI را صدا می‌زند؟
سرویس خارجی از طریق `external_api_adapter.request` صدا زده می‌شود:
```python
response = external_api_request(
"ai",
_get_farm_data_path(),
method="POST",
payload=request_payload,
headers={...},
)
```
### service name
مقدار service برابر است با:
`"ai"`
یعنی این درخواست به سرویسی می‌رود که در تنظیمات به عنوان AI service تعریف شده است.
### base URL سرویس AI
در `config/settings.py`:
```python
"ai": {
"base_url": os.getenv("AI_SERVICE_BASE_URL", "http://ai-web:8000"),
"api_key": os.getenv("AI_SERVICE_API_KEY", ""),
"host_header": os.getenv("AI_SERVICE_HOST_HEADER", "localhost"),
}
```
پس base URL به‌صورت پیش‌فرض این است:
`http://ai-web:8000`
### path مقصد
path از این تنظیم خوانده می‌شود:
```python
FARM_DATA_API_PATH = os.getenv("FARM_DATA_API_PATH", "/api/farm-data/")
```
پس path پیش‌فرض این است:
`/api/farm-data/`
### آدرس نهایی که صدا زده می‌شود
در حالت پیش‌فرض، آدرس نهایی به این صورت است:
`POST http://ai-web:8000/api/farm-data/`
اگر متغیرهای environment تغییر کرده باشند، این آدرس هم تغییر می‌کند.
## چرا این آدرس صدا زده می‌شود؟
هدف از این call این است که داده سنسور خام فقط در بک‌اند ذخیره نشود، بلکه برای پردازش downstream هم به سرویس AI/Farm Data فرستاده شود.
این سرویس AI احتمالا برای کارهای زیر استفاده می‌شود:
- تحلیل داده سنسورها در سطح مزرعه
- ساخت داده تجمیعی farm data
- تغذیه dashboardها و مدل‌های AI
- محاسبه شاخص‌ها یا توصیه‌های بعدی
خود این endpoint در این پروژه فقط داده را forward می‌کند و پردازش AI داخل همین اپ انجام نمی‌شود.
## چه payloadی به AI ارسال می‌شود؟
قبل از ارسال، بک‌اند این ساختار را می‌سازد:
```python
request_payload = {
"farm_uuid": str(sensor.farm.farm_uuid),
"farm_boundary": farm_boundary,
"sensor_payload": {
sensor.name or str(sensor.physical_device_uuid): payload,
},
}
```
یعنی payload ارسال‌شده به AI دقیقا body اولیه کاربر نیست، بلکه این wrapper را دارد:
```json
{
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"farm_boundary": {
"type": "Polygon",
"coordinates": [[[51.39, 35.7], [51.41, 35.7], [51.41, 35.72], [51.39, 35.72], [51.39, 35.7]]]
},
"sensor_payload": {
"Soil Sensor 7-in-1": {
"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
}
}
}
```
## farm_boundary از کجا می‌آید؟
سرویس `_get_farm_boundary` این منطق را دارد:
- اگر `farm.current_crop_area` وجود داشته باشد، از آن استفاده می‌کند.
- اگر وجود نداشته باشد، آخرین crop area مزرعه را برمی‌دارد.
- اگر هیچ boundary وجود نداشته باشد، خطا می‌دهد.
- اگر geometry از نوع `Polygon` نباشد، خطا می‌دهد.
پس سرویس AI فقط وقتی صدا زده می‌شود که مرز مزرعه معتبر وجود داشته باشد.
## هدرهایی که به AI ارسال می‌شوند
در زمان forward کردن، این هدرها ارسال می‌شوند:
```python
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"X-API-Key": api_key,
"Authorization": f"Api-Key {api_key}",
}
```
`api_key` از این setting می‌آید:
`FARM_DATA_API_KEY`
اگر این مقدار ست نشده باشد، پاسخ `503` برمی‌گردد.
## پاسخ موفق
اگر همه چیز درست باشد:
- لاگ ذخیره می‌شود
- notification ساخته می‌شود
- داده به AI forward می‌شود
- پاسخ `201` برمی‌گردد
نمونه ساختار پاسخ:
```json
{
"code": 201,
"msg": "success",
"data": {
"...": "serialized notification object"
}
}
```
نکته:
data خروجی این endpoint نتیجه AI نیست. خروجی، notification ساخته‌شده در سیستم خود بک‌اند است.
## خطاهای ممکن
### 401 Unauthorized
اگر API key ارسال نشود یا اشتباه باشد.
### 404 Not Found
اگر `physical_device_uuid` در جدول `FarmSensor` پیدا نشود.
پاسخ:
```json
{
"code": 404,
"msg": "Physical device not found."
}
```
### 503 Service Unavailable
در چند حالت:
- migration جدول‌ها انجام نشده باشد
- `FARM_DATA_API_KEY` تنظیم نشده باشد
- مرز مزرعه موجود نباشد
- geometry مزرعه `Polygon` نباشد
- سرویس AI در دسترس نباشد
- سرویس AI پاسخ خطای 4xx/5xx بدهد
نمونه خطا:
```json
{
"code": 503,
"msg": "Farm data API request failed: connection error"
}
```
## خلاصه رفتاری endpoint
`POST /api/sensor-external-api/` این کارها را انجام می‌دهد:
1. داده سنسور را از بیرون می‌گیرد.
2. سنسور را با `physical_device_uuid` پیدا می‌کند.
3. payload را در جدول لاگ ذخیره می‌کند.
4. برای مزرعه notification می‌سازد.
5. داده را به سرویس AI در آدرس پیش‌فرض `POST http://ai-web:8000/api/farm-data/` می‌فرستد.
6. در نهایت نتیجه موفقیت را با notification برمی‌گرداند.