UPDATE
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from .models import SensorCatalog
|
||||
from .models import DeviceCatalog
|
||||
|
||||
|
||||
SENSOR_CATALOG_ITEMS = [
|
||||
@@ -10,6 +10,7 @@ SENSOR_CATALOG_ITEMS = [
|
||||
"It measures only soil moisture and provides analog and digital outputs. "
|
||||
"It does not report soil temperature, pH, or nutrients."
|
||||
),
|
||||
"device_communication_type": "output_only",
|
||||
"customizable_fields": [],
|
||||
"supported_power_sources": ["solar", "direct_power"],
|
||||
"returned_data_fields": ["soil_moisture", "analog_output", "digital_output"],
|
||||
@@ -29,11 +30,12 @@ def seed_sensor_catalog():
|
||||
results = []
|
||||
|
||||
for item in SENSOR_CATALOG_ITEMS:
|
||||
sensor, created = SensorCatalog.objects.update_or_create(
|
||||
sensor, created = DeviceCatalog.objects.update_or_create(
|
||||
code=item["code"],
|
||||
defaults={
|
||||
"name": item["name"],
|
||||
"description": item["description"],
|
||||
"device_communication_type": item.get("device_communication_type", "output_only"),
|
||||
"customizable_fields": item["customizable_fields"],
|
||||
"supported_power_sources": item["supported_power_sources"],
|
||||
"returned_data_fields": item["returned_data_fields"],
|
||||
@@ -48,4 +50,3 @@ def seed_sensor_catalog():
|
||||
updated_count += 1
|
||||
|
||||
return results, created_count, updated_count
|
||||
|
||||
|
||||
@@ -7,4 +7,3 @@ urlpatterns = [
|
||||
path("radar-chart/", SensorRadarChartView.as_view(), name="sensor-radar-chart"),
|
||||
path("values-list/", SensorValuesListView.as_view(), name="sensor-values-list"),
|
||||
]
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class Migration(migrations.Migration):
|
||||
options={"db_table": "sensor_catalogs", "ordering": ["code"]},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="FarmSensor",
|
||||
name="FarmDevice",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("uuid", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
|
||||
@@ -47,7 +47,7 @@ class Migration(migrations.Migration):
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("farm", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="sensors", to="farm_hub.farmhub")),
|
||||
("sensor_catalog", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name="farm_sensors", to="device_hub.sensorcatalog")),
|
||||
("sensor_catalog", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name="farm_devices", to="device_hub.sensorcatalog")),
|
||||
],
|
||||
options={"db_table": "farm_sensors", "ordering": ["-created_at"]},
|
||||
),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("device_hub", "0004_absorb_sensor_catalog"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.SeparateDatabaseAndState(
|
||||
database_operations=[],
|
||||
state_operations=[
|
||||
migrations.RenameModel(
|
||||
old_name="FarmSensor",
|
||||
new_name="FarmDevice",
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("device_hub", "0005_rename_farm_sensor_to_farm_device"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="SensorCatalog",
|
||||
new_name="DeviceCatalog",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="devicecatalog",
|
||||
name="device_communication_type",
|
||||
field=models.CharField(
|
||||
choices=[("output_only", "Output Only"), ("input_only", "Input Only")],
|
||||
db_index=True,
|
||||
default="output_only",
|
||||
max_length=32,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="farmdevice",
|
||||
name="sensor_catalog",
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name="farm_devices", to="device_hub.devicecatalog"),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("device_hub", "0006_rename_sensorcatalog_to_devicecatalog_and_add_communication_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="devicecatalog",
|
||||
name="capabilities",
|
||||
field=models.JSONField(blank=True, default=list),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="devicecatalog",
|
||||
name="commands_schema",
|
||||
field=models.JSONField(blank=True, default=list),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="devicecatalog",
|
||||
name="display_schema",
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="devicecatalog",
|
||||
name="payload_mapping",
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="devicecatalog",
|
||||
name="supported_widgets",
|
||||
field=models.JSONField(blank=True, default=list),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def copy_sensor_catalog_to_device_catalogs(apps, schema_editor):
|
||||
FarmDevice = apps.get_model("device_hub", "FarmDevice")
|
||||
for farm_device in FarmDevice.objects.exclude(sensor_catalog__isnull=True).iterator():
|
||||
farm_device.device_catalogs.add(farm_device.sensor_catalog_id)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("device_hub", "0007_devicecatalog_dynamic_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="farmdevice",
|
||||
name="device_catalogs",
|
||||
field=models.ManyToManyField(blank=True, related_name="composite_farm_devices", to="device_hub.devicecatalog"),
|
||||
),
|
||||
migrations.RunPython(copy_sensor_catalog_to_device_catalogs, migrations.RunPython.noop),
|
||||
]
|
||||
+44
-5
@@ -3,14 +3,32 @@ import uuid as uuid_lib
|
||||
from django.db import models
|
||||
|
||||
|
||||
class SensorCatalog(models.Model):
|
||||
class DeviceCatalog(models.Model):
|
||||
OUTPUT_ONLY = "output_only"
|
||||
INPUT_ONLY = "input_only"
|
||||
DEVICE_COMMUNICATION_TYPES = [
|
||||
(OUTPUT_ONLY, "Output Only"),
|
||||
(INPUT_ONLY, "Input Only"),
|
||||
]
|
||||
|
||||
uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
|
||||
code = models.CharField(max_length=255, unique=True, db_index=True)
|
||||
name = models.CharField(max_length=255, unique=True, db_index=True)
|
||||
description = models.TextField(blank=True, default="")
|
||||
device_communication_type = models.CharField(
|
||||
max_length=32,
|
||||
choices=DEVICE_COMMUNICATION_TYPES,
|
||||
default=OUTPUT_ONLY,
|
||||
db_index=True,
|
||||
)
|
||||
customizable_fields = models.JSONField(default=list, blank=True)
|
||||
supported_power_sources = models.JSONField(default=list, blank=True)
|
||||
returned_data_fields = models.JSONField(default=list, blank=True)
|
||||
payload_mapping = models.JSONField(default=dict, blank=True)
|
||||
display_schema = 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)
|
||||
sample_payload = models.JSONField(default=dict, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
@@ -24,16 +42,21 @@ class SensorCatalog(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class FarmSensor(models.Model):
|
||||
class FarmDevice(models.Model):
|
||||
uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
|
||||
farm = models.ForeignKey("farm_hub.FarmHub", on_delete=models.CASCADE, related_name="sensors")
|
||||
sensor_catalog = models.ForeignKey(
|
||||
SensorCatalog,
|
||||
DeviceCatalog,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="farm_sensors",
|
||||
related_name="farm_devices",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
device_catalogs = models.ManyToManyField(
|
||||
DeviceCatalog,
|
||||
related_name="composite_farm_devices",
|
||||
blank=True,
|
||||
)
|
||||
physical_device_uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, db_index=True)
|
||||
name = models.CharField(max_length=255)
|
||||
sensor_type = models.CharField(max_length=255, blank=True, default="")
|
||||
@@ -50,6 +73,23 @@ class FarmSensor(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.uuid})"
|
||||
|
||||
def get_device_catalogs(self):
|
||||
catalogs = list(self.device_catalogs.all())
|
||||
if catalogs:
|
||||
return catalogs
|
||||
if self.sensor_catalog_id:
|
||||
return [self.sensor_catalog]
|
||||
return []
|
||||
|
||||
def get_device_catalog_by_code(self, code):
|
||||
if not code:
|
||||
return None
|
||||
normalized_code = str(code).strip().lower()
|
||||
for catalog in self.get_device_catalogs():
|
||||
if catalog.code.lower() == normalized_code:
|
||||
return catalog
|
||||
return None
|
||||
|
||||
|
||||
class SensorExternalRequestLog(models.Model):
|
||||
farm_uuid = models.UUIDField(db_index=True)
|
||||
@@ -64,4 +104,3 @@ class SensorExternalRequestLog(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.physical_device_uuid}:{self.created_at.isoformat()}"
|
||||
|
||||
|
||||
+4
-3
@@ -6,7 +6,7 @@ from django.utils import timezone
|
||||
|
||||
from farm_hub.seeds import seed_admin_farm
|
||||
|
||||
from .models import FarmSensor, SensorCatalog, SensorExternalRequestLog
|
||||
from .models import DeviceCatalog, FarmDevice, SensorExternalRequestLog
|
||||
|
||||
|
||||
SENSOR_7_IN_1_CATALOG_CODE = "sensor-7-in-1"
|
||||
@@ -23,11 +23,12 @@ SENSOR_7_IN_1_LOG_SERIES = [
|
||||
|
||||
|
||||
def seed_sensor_7_in_1_catalog():
|
||||
sensor_catalog, created = SensorCatalog.objects.update_or_create(
|
||||
sensor_catalog, created = DeviceCatalog.objects.update_or_create(
|
||||
code=SENSOR_7_IN_1_CATALOG_CODE,
|
||||
defaults={
|
||||
"name": "Sensor 7 in 1 Soil Sensor",
|
||||
"description": "Demo 7 in 1 soil sensor for dashboard summary and chart endpoints.",
|
||||
"device_communication_type": "output_only",
|
||||
"customizable_fields": [],
|
||||
"supported_power_sources": ["solar", "battery", "direct_power"],
|
||||
"returned_data_fields": ["soil_moisture", "soil_temperature", "soil_ph", "electrical_conductivity", "nitrogen", "phosphorus", "potassium"],
|
||||
@@ -42,7 +43,7 @@ def seed_sensor_7_in_1_catalog():
|
||||
def seed_sensor_7_in_1_demo_data():
|
||||
farm, _ = seed_admin_farm()
|
||||
sensor_catalog, catalog_created = seed_sensor_7_in_1_catalog()
|
||||
sensor, sensor_created = FarmSensor.objects.update_or_create(
|
||||
sensor, sensor_created = FarmDevice.objects.update_or_create(
|
||||
farm=farm,
|
||||
physical_device_uuid=SENSOR_7_IN_1_DEVICE_UUID,
|
||||
defaults={"sensor_catalog": sensor_catalog, "name": "Sensor 7 in 1 Demo", "sensor_type": "soil_7_in_1", "is_active": True, "specifications": {"capabilities": sensor_catalog.returned_data_fields, "demo_seed": True}, "power_source": {"type": "solar"}},
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import SensorCatalogListView
|
||||
from .views import DeviceCatalogListView
|
||||
|
||||
urlpatterns = [
|
||||
path("", SensorCatalogListView.as_view(), name="sensor-catalog-list"),
|
||||
path("", DeviceCatalogListView.as_view(), name="sensor-catalog-list"),
|
||||
]
|
||||
|
||||
|
||||
@@ -77,3 +77,35 @@ class Sensor7In1SummarySerializer(serializers.Serializer):
|
||||
anomalyDetectionCard = SoilAnomalyDetectionSerializer(required=False)
|
||||
soilMoistureHeatmap = SoilMoistureHeatmapSerializer(required=False)
|
||||
|
||||
|
||||
class DeviceMetaSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(required=False, allow_blank=True)
|
||||
physicalDeviceUuid = serializers.CharField(required=False, allow_null=True)
|
||||
sensorCatalogCode = serializers.CharField(required=False, allow_blank=True)
|
||||
updatedAt = serializers.CharField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class DeviceFieldValueSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(required=False, allow_blank=True)
|
||||
title = serializers.CharField(required=False, allow_blank=True)
|
||||
subtitle = serializers.CharField(required=False, allow_blank=True)
|
||||
trendNumber = serializers.FloatField(required=False)
|
||||
trend = serializers.CharField(required=False, allow_blank=True)
|
||||
unit = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
|
||||
class DeviceValuesListSerializer(serializers.Serializer):
|
||||
sensor = DeviceMetaSerializer(required=False)
|
||||
sensors = DeviceFieldValueSerializer(many=True, required=False)
|
||||
|
||||
|
||||
class DeviceSummarySerializer(serializers.Serializer):
|
||||
sensor = DeviceMetaSerializer(required=False)
|
||||
supportedWidgets = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
sensorValuesList = DeviceValuesListSerializer(required=False)
|
||||
avgSoilMoisture = SoilKpiSerializer(required=False)
|
||||
sensorRadarChart = SoilRadarChartSerializer(required=False)
|
||||
sensorComparisonChart = SoilComparisonChartSerializer(required=False)
|
||||
anomalyDetectionCard = SoilAnomalyDetectionSerializer(required=False)
|
||||
soilMoistureHeatmap = SoilMoistureHeatmapSerializer(required=False)
|
||||
commands = serializers.ListField(child=serializers.JSONField(), required=False)
|
||||
|
||||
+96
-20
@@ -1,19 +1,25 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import FarmSensor, SensorCatalog, SensorExternalRequestLog
|
||||
from .models import DeviceCatalog, FarmDevice, SensorExternalRequestLog
|
||||
|
||||
|
||||
class SensorCatalogSerializer(serializers.ModelSerializer):
|
||||
class DeviceCatalogSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SensorCatalog
|
||||
model = DeviceCatalog
|
||||
fields = [
|
||||
"uuid",
|
||||
"code",
|
||||
"name",
|
||||
"description",
|
||||
"device_communication_type",
|
||||
"customizable_fields",
|
||||
"supported_power_sources",
|
||||
"returned_data_fields",
|
||||
"payload_mapping",
|
||||
"display_schema",
|
||||
"supported_widgets",
|
||||
"commands_schema",
|
||||
"capabilities",
|
||||
"sample_payload",
|
||||
"is_active",
|
||||
]
|
||||
@@ -42,14 +48,16 @@ class SensorExternalRequestLogQuerySerializer(serializers.Serializer):
|
||||
return attrs
|
||||
|
||||
|
||||
class FarmSensorLogSerializer(serializers.ModelSerializer):
|
||||
class FarmDeviceLogSerializer(serializers.ModelSerializer):
|
||||
sensor_catalog_uuid = serializers.UUIDField(source="sensor_catalog.uuid", read_only=True)
|
||||
device_catalogs = DeviceCatalogSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = FarmSensor
|
||||
model = FarmDevice
|
||||
fields = [
|
||||
"uuid",
|
||||
"sensor_catalog_uuid",
|
||||
"device_catalogs",
|
||||
"physical_device_uuid",
|
||||
"name",
|
||||
"sensor_type",
|
||||
@@ -61,17 +69,23 @@ class FarmSensorLogSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SensorCatalogLogSerializer(serializers.ModelSerializer):
|
||||
class DeviceCatalogLogSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SensorCatalog
|
||||
model = DeviceCatalog
|
||||
fields = [
|
||||
"uuid",
|
||||
"code",
|
||||
"name",
|
||||
"description",
|
||||
"device_communication_type",
|
||||
"customizable_fields",
|
||||
"supported_power_sources",
|
||||
"returned_data_fields",
|
||||
"payload_mapping",
|
||||
"display_schema",
|
||||
"supported_widgets",
|
||||
"commands_schema",
|
||||
"capabilities",
|
||||
"sample_payload",
|
||||
"is_active",
|
||||
"created_at",
|
||||
@@ -79,8 +93,70 @@ class SensorCatalogLogSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class DeviceDetailSerializer(serializers.ModelSerializer):
|
||||
device_catalog = DeviceCatalogSerializer(source="sensor_catalog", read_only=True)
|
||||
device_catalogs = serializers.SerializerMethodField()
|
||||
last_payload_at = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = FarmDevice
|
||||
fields = [
|
||||
"uuid",
|
||||
"physical_device_uuid",
|
||||
"name",
|
||||
"sensor_type",
|
||||
"is_active",
|
||||
"specifications",
|
||||
"power_source",
|
||||
"device_catalog",
|
||||
"device_catalogs",
|
||||
"last_payload_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def get_last_payload_at(self, obj):
|
||||
latest_log = self.context.get("latest_log")
|
||||
if latest_log is None:
|
||||
return None
|
||||
return latest_log.created_at
|
||||
|
||||
def get_device_catalogs(self, obj):
|
||||
return DeviceCatalogSerializer(obj.get_device_catalogs(), many=True).data
|
||||
|
||||
|
||||
class DeviceLatestPayloadSerializer(serializers.Serializer):
|
||||
physical_device_uuid = serializers.UUIDField()
|
||||
device_code = serializers.CharField()
|
||||
device_catalog_code = serializers.CharField(allow_blank=True, allow_null=True)
|
||||
raw_payload = serializers.JSONField()
|
||||
normalized_payload = serializers.JSONField()
|
||||
readings = serializers.JSONField()
|
||||
created_at = serializers.DateTimeField(allow_null=True)
|
||||
|
||||
|
||||
class DeviceCommandRequestSerializer(serializers.Serializer):
|
||||
device_code = serializers.CharField()
|
||||
command = serializers.CharField()
|
||||
payload = serializers.JSONField(required=False, default=dict)
|
||||
|
||||
|
||||
class DeviceCodeQuerySerializer(serializers.Serializer):
|
||||
device_code = serializers.CharField()
|
||||
|
||||
|
||||
class DeviceRangeQuerySerializer(DeviceCodeQuerySerializer):
|
||||
range = serializers.CharField()
|
||||
|
||||
|
||||
class DeviceCommandResponseSerializer(serializers.Serializer):
|
||||
physical_device_uuid = serializers.UUIDField()
|
||||
command = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
|
||||
|
||||
class SensorExternalRequestLogSerializer(serializers.ModelSerializer):
|
||||
farm_sensor = serializers.SerializerMethodField()
|
||||
farm_device = serializers.SerializerMethodField()
|
||||
sensor_catalog = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
@@ -90,26 +166,26 @@ class SensorExternalRequestLogSerializer(serializers.ModelSerializer):
|
||||
"farm_uuid",
|
||||
"sensor_catalog_uuid",
|
||||
"physical_device_uuid",
|
||||
"farm_sensor",
|
||||
"farm_device",
|
||||
"sensor_catalog",
|
||||
"payload",
|
||||
"created_at",
|
||||
]
|
||||
|
||||
def get_farm_sensor(self, obj):
|
||||
farm_sensor_map = self.context.get("farm_sensor_map", {})
|
||||
farm_sensor = farm_sensor_map.get(
|
||||
def get_farm_device(self, obj):
|
||||
farm_device_map = self.context.get("farm_device_map", {})
|
||||
farm_device = farm_device_map.get(
|
||||
(obj.farm_uuid, obj.sensor_catalog_uuid, obj.physical_device_uuid)
|
||||
) or farm_sensor_map.get((obj.farm_uuid, None, obj.physical_device_uuid))
|
||||
if farm_sensor is None:
|
||||
) or farm_device_map.get((obj.farm_uuid, None, obj.physical_device_uuid))
|
||||
if farm_device is None:
|
||||
return None
|
||||
return FarmSensorLogSerializer(farm_sensor).data
|
||||
return FarmDeviceLogSerializer(farm_device).data
|
||||
|
||||
def get_sensor_catalog(self, obj):
|
||||
farm_sensor_map = self.context.get("farm_sensor_map", {})
|
||||
farm_sensor = farm_sensor_map.get(
|
||||
farm_device_map = self.context.get("farm_device_map", {})
|
||||
farm_device = farm_device_map.get(
|
||||
(obj.farm_uuid, obj.sensor_catalog_uuid, obj.physical_device_uuid)
|
||||
) or farm_sensor_map.get((obj.farm_uuid, None, obj.physical_device_uuid))
|
||||
if farm_sensor is None or farm_sensor.sensor_catalog is None:
|
||||
) or farm_device_map.get((obj.farm_uuid, None, obj.physical_device_uuid))
|
||||
if farm_device is None or farm_device.sensor_catalog is None:
|
||||
return None
|
||||
return SensorCatalogLogSerializer(farm_sensor.sensor_catalog).data
|
||||
return DeviceCatalogLogSerializer(farm_device.sensor_catalog).data
|
||||
|
||||
+517
-24
@@ -11,7 +11,7 @@ from external_api_adapter.exceptions import ExternalAPIRequestError
|
||||
from notifications.services import create_notification_for_farm_uuid
|
||||
|
||||
from .mock_data import ANOMALY_DETECTION_CARD, AVG_SOIL_MOISTURE, SENSOR_COMPARISON_CHART, SENSOR_RADAR_CHART, SENSOR_VALUES_LIST, SOIL_MOISTURE_HEATMAP
|
||||
from .models import FarmSensor, SensorExternalRequestLog
|
||||
from .models import FarmDevice, SensorExternalRequestLog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,7 +46,7 @@ def get_sensor_external_request_logs_for_farm(*, farm_uuid, physical_device_uuid
|
||||
if physical_device_uuid:
|
||||
queryset = queryset.filter(physical_device_uuid=physical_device_uuid)
|
||||
if sensor_type:
|
||||
physical_device_uuids = FarmSensor.objects.filter(farm__farm_uuid=farm_uuid, sensor_type=sensor_type).values_list("physical_device_uuid", flat=True)
|
||||
physical_device_uuids = FarmDevice.objects.filter(farm__farm_uuid=farm_uuid, sensor_type=sensor_type).values_list("physical_device_uuid", flat=True)
|
||||
queryset = queryset.filter(physical_device_uuid__in=physical_device_uuids)
|
||||
if date_from:
|
||||
queryset = queryset.filter(created_at__date__gte=date_from)
|
||||
@@ -57,22 +57,22 @@ def get_sensor_external_request_logs_for_farm(*, farm_uuid, physical_device_uuid
|
||||
raise ValueError("Sensor external API tables are not migrated.") from exc
|
||||
|
||||
|
||||
def get_farm_sensor_map_for_logs(*, logs):
|
||||
def get_farm_device_map_for_logs(*, logs):
|
||||
try:
|
||||
logs = list(logs)
|
||||
if not logs:
|
||||
return {}
|
||||
farm_sensor_queryset = FarmSensor.objects.select_related("farm", "sensor_catalog").filter(
|
||||
farm_device_queryset = FarmDevice.objects.select_related("farm", "sensor_catalog").filter(
|
||||
farm__farm_uuid__in={log.farm_uuid for log in logs},
|
||||
physical_device_uuid__in={log.physical_device_uuid for log in logs},
|
||||
).order_by("-created_at", "-id")
|
||||
farm_sensor_map = {}
|
||||
for farm_sensor in farm_sensor_queryset:
|
||||
exact_key = (farm_sensor.farm.farm_uuid, farm_sensor.sensor_catalog.uuid if farm_sensor.sensor_catalog else None, farm_sensor.physical_device_uuid)
|
||||
fallback_key = (farm_sensor.farm.farm_uuid, None, farm_sensor.physical_device_uuid)
|
||||
farm_sensor_map.setdefault(exact_key, farm_sensor)
|
||||
farm_sensor_map.setdefault(fallback_key, farm_sensor)
|
||||
return farm_sensor_map
|
||||
farm_device_map = {}
|
||||
for farm_device in farm_device_queryset:
|
||||
exact_key = (farm_device.farm.farm_uuid, farm_device.sensor_catalog.uuid if farm_device.sensor_catalog else None, farm_device.physical_device_uuid)
|
||||
fallback_key = (farm_device.farm.farm_uuid, None, farm_device.physical_device_uuid)
|
||||
farm_device_map.setdefault(exact_key, farm_device)
|
||||
farm_device_map.setdefault(fallback_key, farm_device)
|
||||
return farm_device_map
|
||||
except (ProgrammingError, OperationalError) as exc:
|
||||
raise ValueError("Sensor external API tables are not migrated.") from exc
|
||||
|
||||
@@ -86,7 +86,7 @@ def get_latest_sensor_external_request_log(*, farm_uuid, sensor_catalog_uuid, ph
|
||||
|
||||
def create_sensor_external_notification(*, physical_device_uuid, payload=None):
|
||||
payload = payload or {}
|
||||
sensor = FarmSensor.objects.select_related("farm", "farm__current_crop_area", "sensor_catalog").filter(physical_device_uuid=physical_device_uuid).first()
|
||||
sensor = FarmDevice.objects.select_related("farm", "farm__current_crop_area", "sensor_catalog").filter(physical_device_uuid=physical_device_uuid).first()
|
||||
if sensor is None:
|
||||
raise ValueError("Physical device not found.")
|
||||
try:
|
||||
@@ -110,7 +110,7 @@ def create_sensor_external_notification(*, physical_device_uuid, payload=None):
|
||||
|
||||
def forward_sensor_payload_to_farm_data(*, physical_device_uuid, payload=None):
|
||||
payload = payload or {}
|
||||
sensor = FarmSensor.objects.select_related("farm", "farm__current_crop_area", "sensor_catalog").filter(physical_device_uuid=physical_device_uuid).first()
|
||||
sensor = FarmDevice.objects.select_related("farm", "farm__current_crop_area", "sensor_catalog").filter(physical_device_uuid=physical_device_uuid).first()
|
||||
if sensor is None:
|
||||
raise ValueError("Physical device not found.")
|
||||
farm_boundary = _get_farm_boundary(sensor=sensor)
|
||||
@@ -273,23 +273,23 @@ def _get_sensor_context(farm=None):
|
||||
if not history:
|
||||
return None
|
||||
latest_log, latest_readings = history[0]
|
||||
farm_sensor_map = get_farm_sensor_map_for_logs(logs=[latest_log])
|
||||
farm_sensor = farm_sensor_map.get((latest_log.farm_uuid, latest_log.sensor_catalog_uuid, latest_log.physical_device_uuid)) or primary_sensor
|
||||
return {"farm_sensor": farm_sensor, "latest_log": latest_log, "latest_readings": latest_readings, "previous_readings": history[1][1] if len(history) > 1 else {}, "history": history}
|
||||
farm_device_map = get_farm_device_map_for_logs(logs=[latest_log])
|
||||
farm_device = farm_device_map.get((latest_log.farm_uuid, latest_log.sensor_catalog_uuid, latest_log.physical_device_uuid)) or primary_sensor
|
||||
return {"farm_device": farm_device, "latest_log": latest_log, "latest_readings": latest_readings, "previous_readings": history[1][1] if len(history) > 1 else {}, "history": history}
|
||||
|
||||
|
||||
def _build_sensor_meta(context, fallback_sensor):
|
||||
sensor = deepcopy(fallback_sensor)
|
||||
if not context:
|
||||
return sensor
|
||||
farm_sensor = context.get("farm_sensor")
|
||||
farm_device = context.get("farm_device")
|
||||
latest_log = context["latest_log"]
|
||||
sensor["physicalDeviceUuid"] = str(latest_log.physical_device_uuid)
|
||||
sensor["updatedAt"] = latest_log.created_at.isoformat()
|
||||
if farm_sensor is not None:
|
||||
sensor["name"] = farm_sensor.name or sensor["name"]
|
||||
if farm_sensor.sensor_catalog is not None:
|
||||
sensor["sensorCatalogCode"] = farm_sensor.sensor_catalog.code
|
||||
if farm_device is not None:
|
||||
sensor["name"] = farm_device.name or sensor["name"]
|
||||
if farm_device.sensor_catalog is not None:
|
||||
sensor["sensorCatalogCode"] = farm_device.sensor_catalog.code
|
||||
return sensor
|
||||
|
||||
|
||||
@@ -434,9 +434,9 @@ def get_sensor_7_in_1_soil_moisture_heatmap_data(farm=None, context=None):
|
||||
if not chart_points:
|
||||
return data
|
||||
sensor_name = data["zones"][0]
|
||||
farm_sensor = context.get("farm_sensor")
|
||||
if farm_sensor is not None and farm_sensor.name:
|
||||
sensor_name = farm_sensor.name
|
||||
farm_device = context.get("farm_device")
|
||||
if farm_device is not None and farm_device.name:
|
||||
sensor_name = farm_device.name
|
||||
data["zones"] = [sensor_name]
|
||||
data["hours"] = [point["x"] for point in chart_points]
|
||||
data["series"] = [{"name": sensor_name, "data": chart_points}]
|
||||
@@ -557,3 +557,496 @@ def get_sensor_radar_chart_data(*, farm, physical_device_uuid, range_value):
|
||||
current_data.append(round(current_value, 2))
|
||||
ideal_data.append(round(ideal_value, 2))
|
||||
return {"labels": labels, "series": [{"name": "وضعیت فعلی", "data": current_data}, {"name": "بازه ایده آل", "data": ideal_data}]}
|
||||
|
||||
|
||||
DEVICE_COMMAND_PAYLOAD_TYPES = {
|
||||
"string": str,
|
||||
"integer": int,
|
||||
"number": (int, float),
|
||||
"boolean": bool,
|
||||
"object": dict,
|
||||
"array": list,
|
||||
}
|
||||
DEFAULT_DEVICE_WIDGETS = [
|
||||
"values_list",
|
||||
"comparison_chart",
|
||||
"radar_chart",
|
||||
"latest_payload",
|
||||
"anomaly_card",
|
||||
"soil_moisture_heatmap",
|
||||
]
|
||||
|
||||
|
||||
def get_farm_device_by_physical_uuid(*, physical_device_uuid, owner=None):
|
||||
queryset = FarmDevice.objects.select_related("farm", "sensor_catalog").filter(physical_device_uuid=physical_device_uuid)
|
||||
if owner is not None:
|
||||
queryset = queryset.filter(farm__owner=owner)
|
||||
return queryset.first()
|
||||
|
||||
|
||||
def get_device_catalog_for_farm_device(farm_device, *, device_code=None):
|
||||
if farm_device is None:
|
||||
return None
|
||||
if device_code:
|
||||
return farm_device.get_device_catalog_by_code(device_code)
|
||||
return farm_device.sensor_catalog if farm_device.sensor_catalog_id else (farm_device.get_device_catalogs()[0] if farm_device.get_device_catalogs() else None)
|
||||
|
||||
|
||||
def get_latest_device_log(farm_device, *, device_catalog=None):
|
||||
if farm_device is None:
|
||||
return None
|
||||
return get_latest_sensor_external_request_log(
|
||||
farm_uuid=farm_device.farm.farm_uuid,
|
||||
sensor_catalog_uuid=device_catalog.uuid if device_catalog else (farm_device.sensor_catalog.uuid if farm_device.sensor_catalog else None),
|
||||
physical_device_uuid=farm_device.physical_device_uuid,
|
||||
)
|
||||
|
||||
|
||||
def get_device_logs(farm_device, *, range_value=None, date_from=None, date_to=None):
|
||||
if farm_device is None:
|
||||
return SensorExternalRequestLog.objects.none()
|
||||
if range_value:
|
||||
date_from = timezone.localdate() - timedelta(days=max(range_value - 1, 0))
|
||||
return get_sensor_external_request_logs_for_farm(
|
||||
farm_uuid=farm_device.farm.farm_uuid,
|
||||
physical_device_uuid=farm_device.physical_device_uuid,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
)
|
||||
|
||||
|
||||
def validate_output_device_catalog(*, farm_device, device_code):
|
||||
device_catalog = get_device_catalog_for_farm_device(farm_device, device_code=device_code)
|
||||
if device_catalog is None:
|
||||
raise ValueError("Device code is not attached to this farm device.")
|
||||
if device_catalog.device_communication_type == "input_only":
|
||||
raise ValueError("Selected device code is input-only and cannot be used for output data endpoints.")
|
||||
return device_catalog
|
||||
|
||||
|
||||
def _get_default_field_definition_map():
|
||||
return {field["id"]: field for field in SENSOR_FIELDS}
|
||||
|
||||
|
||||
def _normalize_payload_keys(payload_keys):
|
||||
if isinstance(payload_keys, str):
|
||||
return [payload_keys]
|
||||
if isinstance(payload_keys, (list, tuple)):
|
||||
return [item for item in payload_keys if isinstance(item, str) and item]
|
||||
return []
|
||||
|
||||
|
||||
def _get_device_field_definitions(device_catalog):
|
||||
default_field_map = _get_default_field_definition_map()
|
||||
if device_catalog is None:
|
||||
return list(default_field_map.values())
|
||||
|
||||
payload_mapping = device_catalog.payload_mapping if isinstance(device_catalog.payload_mapping, dict) else {}
|
||||
display_schema = device_catalog.display_schema if isinstance(device_catalog.display_schema, dict) else {}
|
||||
display_fields = display_schema.get("fields", []) if isinstance(display_schema.get("fields", []), list) else []
|
||||
|
||||
ordered_ids = []
|
||||
for item in display_fields:
|
||||
if isinstance(item, dict) and item.get("id"):
|
||||
ordered_ids.append(item["id"])
|
||||
for item in device_catalog.returned_data_fields:
|
||||
if isinstance(item, str) and item not in ordered_ids:
|
||||
ordered_ids.append(item)
|
||||
for item in payload_mapping.keys():
|
||||
if item not in ordered_ids:
|
||||
ordered_ids.append(item)
|
||||
if not ordered_ids:
|
||||
ordered_ids = list(default_field_map.keys())
|
||||
|
||||
display_field_map = {
|
||||
item["id"]: item for item in display_fields if isinstance(item, dict) and item.get("id")
|
||||
}
|
||||
field_definitions = []
|
||||
for field_id in ordered_ids:
|
||||
default_field = default_field_map.get(field_id, {})
|
||||
display_field = display_field_map.get(field_id, {})
|
||||
payload_keys = _normalize_payload_keys(payload_mapping.get(field_id)) or list(default_field.get("payload_keys", [])) or [field_id]
|
||||
field_definitions.append(
|
||||
{
|
||||
"id": field_id,
|
||||
"label": display_field.get("label") or default_field.get("label") or field_id.replace("_", " ").title(),
|
||||
"unit": display_field.get("unit") or default_field.get("unit") or "",
|
||||
"payload_keys": payload_keys,
|
||||
"ideal_min": display_field.get("ideal_min", default_field.get("ideal_min", 0.0)),
|
||||
"ideal_max": display_field.get("ideal_max", default_field.get("ideal_max", 100.0)),
|
||||
"radar_label": display_field.get("radar_label") or default_field.get("radar_label") or display_field.get("label") or default_field.get("label") or field_id,
|
||||
}
|
||||
)
|
||||
return field_definitions
|
||||
|
||||
|
||||
def _extract_payload_with_field_definitions(payload, field_definitions):
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
if isinstance(payload.get("payload"), dict):
|
||||
payload = payload["payload"]
|
||||
expected_keys = {key for field in field_definitions for key in field.get("payload_keys", [])}
|
||||
if isinstance(payload.get("data"), dict):
|
||||
nested = payload["data"]
|
||||
if not expected_keys or any(key in nested for key in expected_keys):
|
||||
payload = nested
|
||||
return payload
|
||||
|
||||
|
||||
def normalize_device_payload(device_catalog, payload):
|
||||
field_definitions = _get_device_field_definitions(device_catalog)
|
||||
payload = _extract_payload_with_field_definitions(payload, field_definitions)
|
||||
normalized_payload = {}
|
||||
for field in field_definitions:
|
||||
for key in field["payload_keys"]:
|
||||
if key in payload:
|
||||
normalized_payload[field["id"]] = payload[key]
|
||||
break
|
||||
return normalized_payload
|
||||
|
||||
|
||||
def extract_device_readings(device_catalog, payload):
|
||||
normalized_payload = normalize_device_payload(device_catalog, payload)
|
||||
readings = {}
|
||||
for key, value in normalized_payload.items():
|
||||
numeric_value = _to_float(value)
|
||||
if numeric_value is not None:
|
||||
readings[key] = numeric_value
|
||||
return readings
|
||||
|
||||
|
||||
def _get_device_supported_widgets(device_catalog):
|
||||
if device_catalog is None:
|
||||
return list(DEFAULT_DEVICE_WIDGETS)
|
||||
widgets = device_catalog.supported_widgets if isinstance(device_catalog.supported_widgets, list) else []
|
||||
if widgets:
|
||||
return widgets
|
||||
if device_catalog.device_communication_type == "input_only":
|
||||
return []
|
||||
return list(DEFAULT_DEVICE_WIDGETS)
|
||||
|
||||
|
||||
def _get_device_history_context(farm_device):
|
||||
if farm_device is None:
|
||||
return None
|
||||
try:
|
||||
logs_queryset = get_device_logs(farm_device)
|
||||
except ValueError:
|
||||
return None
|
||||
history = []
|
||||
device_catalog = get_device_catalog_for_farm_device(farm_device)
|
||||
for log in logs_queryset[:MAX_HISTORY_ITEMS]:
|
||||
readings = extract_device_readings(device_catalog, log.payload)
|
||||
normalized_payload = normalize_device_payload(device_catalog, log.payload)
|
||||
if readings or normalized_payload:
|
||||
history.append((log, readings, normalized_payload))
|
||||
if not history:
|
||||
return {
|
||||
"farm_device": farm_device,
|
||||
"latest_log": None,
|
||||
"latest_readings": {},
|
||||
"latest_payload": {},
|
||||
"previous_readings": {},
|
||||
"history": [],
|
||||
}
|
||||
latest_log, latest_readings, latest_payload = history[0]
|
||||
return {
|
||||
"farm_device": farm_device,
|
||||
"latest_log": latest_log,
|
||||
"latest_readings": latest_readings,
|
||||
"latest_payload": latest_payload,
|
||||
"previous_readings": history[1][1] if len(history) > 1 else {},
|
||||
"history": history,
|
||||
}
|
||||
|
||||
|
||||
def build_device_meta(farm_device, context=None, *, device_catalog=None):
|
||||
device_catalog = device_catalog or get_device_catalog_for_farm_device(farm_device)
|
||||
latest_log = (context or {}).get("latest_log")
|
||||
return {
|
||||
"name": farm_device.name if farm_device else "",
|
||||
"physicalDeviceUuid": str(farm_device.physical_device_uuid) if farm_device else None,
|
||||
"sensorCatalogCode": device_catalog.code if device_catalog else "",
|
||||
"updatedAt": latest_log.created_at.isoformat() if latest_log else None,
|
||||
}
|
||||
|
||||
|
||||
def build_device_latest_payload(farm_device, *, device_code):
|
||||
device_catalog = validate_output_device_catalog(farm_device=farm_device, device_code=device_code)
|
||||
latest_log = get_latest_device_log(farm_device, device_catalog=device_catalog)
|
||||
if latest_log is None:
|
||||
return {
|
||||
"physical_device_uuid": farm_device.physical_device_uuid,
|
||||
"device_code": device_code,
|
||||
"device_catalog_code": device_catalog.code if device_catalog else None,
|
||||
"raw_payload": {},
|
||||
"normalized_payload": {},
|
||||
"readings": {},
|
||||
"created_at": None,
|
||||
}
|
||||
return {
|
||||
"physical_device_uuid": farm_device.physical_device_uuid,
|
||||
"device_code": device_code,
|
||||
"device_catalog_code": device_catalog.code if device_catalog else None,
|
||||
"raw_payload": latest_log.payload,
|
||||
"normalized_payload": normalize_device_payload(device_catalog, latest_log.payload),
|
||||
"readings": extract_device_readings(device_catalog, latest_log.payload),
|
||||
"created_at": latest_log.created_at,
|
||||
}
|
||||
|
||||
|
||||
def build_device_values_list(farm_device, range_value, *, device_code):
|
||||
try:
|
||||
logs_queryset = get_device_logs(farm_device)
|
||||
except ValueError:
|
||||
return {"sensors": []}
|
||||
start_time = timezone.now() - VALUES_LIST_RANGES[range_value]
|
||||
logs = list(logs_queryset.filter(created_at__gte=start_time).order_by("created_at", "id"))
|
||||
if not logs:
|
||||
latest_log = logs_queryset.order_by("-created_at", "-id").first()
|
||||
if latest_log is None:
|
||||
return {"sensors": []}
|
||||
logs = [latest_log]
|
||||
device_catalog = validate_output_device_catalog(farm_device=farm_device, device_code=device_code)
|
||||
earliest_payload = {}
|
||||
latest_payload = {}
|
||||
for log in logs:
|
||||
normalized_payload = extract_device_readings(device_catalog, log.payload)
|
||||
if not normalized_payload:
|
||||
continue
|
||||
if not earliest_payload:
|
||||
earliest_payload = normalized_payload
|
||||
latest_payload = normalized_payload
|
||||
if not latest_payload:
|
||||
return {"sensors": []}
|
||||
sensors = []
|
||||
for field in _get_device_field_definitions(device_catalog):
|
||||
current_value = latest_payload.get(field["id"])
|
||||
if current_value is None:
|
||||
continue
|
||||
previous_value = earliest_payload.get(field["id"], current_value)
|
||||
delta = round(current_value - previous_value, 2)
|
||||
sensors.append(
|
||||
{
|
||||
"title": field["label"],
|
||||
"subtitle": _format_current_value_subtitle(field["label"], current_value, field["unit"]),
|
||||
"trendNumber": abs(delta),
|
||||
"trend": "positive" if delta >= 0 else "negative",
|
||||
"unit": field["unit"],
|
||||
}
|
||||
)
|
||||
return {"sensors": sensors}
|
||||
|
||||
|
||||
def build_device_summary_values_list(farm_device, context=None, *, device_catalog=None):
|
||||
context = _get_device_history_context(farm_device) if context is None else context
|
||||
device_catalog = device_catalog or get_device_catalog_for_farm_device(farm_device)
|
||||
data = {"sensor": build_device_meta(farm_device, context), "sensors": []}
|
||||
latest_readings = context.get("latest_readings", {}) if context else {}
|
||||
previous_readings = context.get("previous_readings", {}) if context else {}
|
||||
for field in _get_device_field_definitions(device_catalog):
|
||||
value = latest_readings.get(field["id"])
|
||||
if value is None:
|
||||
continue
|
||||
previous = previous_readings.get(field["id"])
|
||||
change = 0.0 if previous is None else round(value - previous, 2)
|
||||
data["sensors"].append(
|
||||
{
|
||||
"id": field["id"],
|
||||
"title": _format_value(value, field["unit"]),
|
||||
"subtitle": field["label"],
|
||||
"trendNumber": abs(change),
|
||||
"trend": "positive" if change >= 0 else "negative",
|
||||
"unit": field["unit"],
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
def build_device_radar_chart(farm_device, range_value=None, *, device_code):
|
||||
device_catalog = validate_output_device_catalog(farm_device=farm_device, device_code=device_code)
|
||||
context = _get_device_history_context(farm_device)
|
||||
if not context or not context.get("latest_readings"):
|
||||
return {"labels": [], "series": []}
|
||||
labels, current_data, ideal_data = [], [], []
|
||||
for field in _get_device_field_definitions(device_catalog):
|
||||
current_value = context["latest_readings"].get(field["id"])
|
||||
if current_value is None:
|
||||
continue
|
||||
labels.append(field["radar_label"])
|
||||
current_data.append(round(current_value, 2))
|
||||
midpoint = (field["ideal_min"] + field["ideal_max"]) / 2
|
||||
ideal_data.append(round(midpoint, 2))
|
||||
return {"labels": labels, "series": [{"name": "وضعیت فعلی", "data": current_data}, {"name": "بازه ایده آل", "data": ideal_data}]}
|
||||
|
||||
|
||||
def build_device_comparison_chart(farm_device, range_value, *, device_code):
|
||||
days = COMPARISON_CHART_RANGES[range_value]
|
||||
start_date = timezone.localdate() - timedelta(days=days - 1)
|
||||
try:
|
||||
logs_queryset = get_device_logs(farm_device, date_from=start_date)
|
||||
except ValueError:
|
||||
return {"series": [], "categories": [], "currentValue": 0.0, "vsLastWeek": "+0.0%"}
|
||||
device_catalog = validate_output_device_catalog(farm_device=farm_device, device_code=device_code)
|
||||
field_definitions = _get_device_field_definitions(device_catalog)
|
||||
grouped_logs = {}
|
||||
for log in reversed(list(logs_queryset[: days * 24])):
|
||||
bucket_date = timezone.localtime(log.created_at).date()
|
||||
grouped_logs[bucket_date] = extract_device_readings(device_catalog, log.payload)
|
||||
if not grouped_logs:
|
||||
return {"series": [], "categories": [], "currentValue": 0.0, "vsLastWeek": "+0.0%"}
|
||||
sorted_dates = sorted(grouped_logs.keys())
|
||||
categories = [_format_comparison_category(bucket_date, range_value) for bucket_date in sorted_dates]
|
||||
series = []
|
||||
primary_data = []
|
||||
for field in field_definitions:
|
||||
data_points = []
|
||||
for bucket_date in sorted_dates:
|
||||
value = grouped_logs[bucket_date].get(field["id"])
|
||||
if value is None:
|
||||
data_points.append(0.0)
|
||||
else:
|
||||
data_points.append(round(value, 2))
|
||||
if any(point != 0.0 for point in data_points):
|
||||
series.append({"name": field["label"], "data": data_points})
|
||||
if not primary_data:
|
||||
primary_data = data_points
|
||||
if not series or not primary_data:
|
||||
return {"series": [], "categories": [], "currentValue": 0.0, "vsLastWeek": "+0.0%"}
|
||||
return {
|
||||
"series": series,
|
||||
"categories": categories,
|
||||
"currentValue": round(primary_data[-1], 2),
|
||||
"vsLastWeek": _format_percent_change(primary_data[-1], primary_data[0]),
|
||||
}
|
||||
|
||||
|
||||
def build_device_anomaly_detection_card(farm_device, context=None, *, device_catalog=None):
|
||||
context = _get_device_history_context(farm_device) if context is None else context
|
||||
device_catalog = device_catalog or get_device_catalog_for_farm_device(farm_device)
|
||||
anomalies = []
|
||||
latest_readings = context.get("latest_readings", {}) if context else {}
|
||||
for field in _get_device_field_definitions(device_catalog):
|
||||
value = latest_readings.get(field["id"])
|
||||
if value is None:
|
||||
continue
|
||||
anomaly = _build_anomaly_item(field, value)
|
||||
if anomaly is not None:
|
||||
anomalies.append(anomaly)
|
||||
return {
|
||||
"anomalies": anomalies or [
|
||||
{
|
||||
"sensor": farm_device.name if farm_device else "Device",
|
||||
"value": "نرمال",
|
||||
"expected": "تمام شاخصها در بازه مجاز هستند",
|
||||
"deviation": "0",
|
||||
"severity": "success",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def build_device_soil_moisture_heatmap(farm_device, context=None, *, device_catalog=None):
|
||||
data = deepcopy(SOIL_MOISTURE_HEATMAP)
|
||||
context = _get_device_history_context(farm_device) if context is None else context
|
||||
if not context or not context.get("history"):
|
||||
return data
|
||||
device_catalog = device_catalog or get_device_catalog_for_farm_device(farm_device)
|
||||
field_definitions = _get_device_field_definitions(device_catalog)
|
||||
primary_field = field_definitions[0] if field_definitions else None
|
||||
if primary_field is None:
|
||||
return data
|
||||
chart_points = []
|
||||
for log, readings, _normalized_payload in reversed(context["history"][:MAX_CHART_POINTS]):
|
||||
value = readings.get(primary_field["id"])
|
||||
if value is None:
|
||||
continue
|
||||
chart_points.append({"x": log.created_at.strftime("%H:%M"), "y": round(value, 2)})
|
||||
if not chart_points:
|
||||
return data
|
||||
sensor_name = farm_device.name if farm_device and farm_device.name else data["zones"][0]
|
||||
data["zones"] = [sensor_name]
|
||||
data["hours"] = [point["x"] for point in chart_points]
|
||||
data["series"] = [{"name": sensor_name, "data": chart_points}]
|
||||
return data
|
||||
|
||||
|
||||
def build_device_avg_primary_metric(farm_device, context=None, *, device_catalog=None):
|
||||
data = deepcopy(AVG_SOIL_MOISTURE)
|
||||
context = _get_device_history_context(farm_device) if context is None else context
|
||||
latest_readings = context.get("latest_readings", {}) if context else {}
|
||||
device_catalog = device_catalog or get_device_catalog_for_farm_device(farm_device)
|
||||
field_definitions = _get_device_field_definitions(device_catalog)
|
||||
primary_field = field_definitions[0] if field_definitions else None
|
||||
if primary_field is None:
|
||||
return data
|
||||
primary_value = latest_readings.get(primary_field["id"])
|
||||
if primary_value is None:
|
||||
return data
|
||||
chip_text, chip_color, avatar_color = _calculate_status_chip(primary_value)
|
||||
data["title"] = primary_field["label"]
|
||||
data["stats"] = _format_value(primary_value, primary_field["unit"])
|
||||
data["chipText"] = chip_text
|
||||
data["chipColor"] = chip_color
|
||||
data["avatarColor"] = avatar_color
|
||||
return data
|
||||
|
||||
|
||||
def build_device_summary(farm_device, *, device_code):
|
||||
context = _get_device_history_context(farm_device)
|
||||
device_catalog = validate_output_device_catalog(farm_device=farm_device, device_code=device_code)
|
||||
summary = {"sensor": build_device_meta(farm_device, context, device_catalog=device_catalog), "supportedWidgets": _get_device_supported_widgets(device_catalog)}
|
||||
if device_catalog and device_catalog.device_communication_type == "input_only":
|
||||
summary["commands"] = device_catalog.commands_schema if isinstance(device_catalog.commands_schema, list) else []
|
||||
return summary
|
||||
summary["sensorValuesList"] = build_device_summary_values_list(farm_device, context=context, device_catalog=device_catalog)
|
||||
if "comparison_chart" in summary["supportedWidgets"]:
|
||||
summary["sensorComparisonChart"] = build_device_comparison_chart(farm_device, "7d", device_code=device_code)
|
||||
if "radar_chart" in summary["supportedWidgets"]:
|
||||
summary["sensorRadarChart"] = build_device_radar_chart(farm_device, device_code=device_code)
|
||||
if "anomaly_card" in summary["supportedWidgets"]:
|
||||
summary["anomalyDetectionCard"] = build_device_anomaly_detection_card(farm_device, context=context, device_catalog=device_catalog)
|
||||
if "soil_moisture_heatmap" in summary["supportedWidgets"]:
|
||||
summary["soilMoistureHeatmap"] = build_device_soil_moisture_heatmap(farm_device, context=context, device_catalog=device_catalog)
|
||||
summary["avgSoilMoisture"] = build_device_avg_primary_metric(farm_device, context=context, device_catalog=device_catalog)
|
||||
return summary
|
||||
|
||||
|
||||
def validate_device_command(farm_device, command, payload, *, device_code):
|
||||
device_catalog = validate_output_device_catalog(farm_device=farm_device, device_code=device_code)
|
||||
commands_schema = device_catalog.commands_schema if device_catalog and isinstance(device_catalog.commands_schema, list) else []
|
||||
if not commands_schema:
|
||||
raise ValueError("This device does not support commands.")
|
||||
matched_command = next(
|
||||
(item for item in commands_schema if isinstance(item, dict) and item.get("command") == command),
|
||||
None,
|
||||
)
|
||||
if matched_command is None:
|
||||
raise ValueError("Command is not supported for this device.")
|
||||
payload = payload or {}
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("`payload` must be an object.")
|
||||
payload_schema = matched_command.get("payload_schema", {})
|
||||
if not isinstance(payload_schema, dict):
|
||||
return matched_command
|
||||
for key, expected_type in payload_schema.items():
|
||||
if key not in payload:
|
||||
raise ValueError(f"`{key}` is required for this command.")
|
||||
expected_python_type = DEVICE_COMMAND_PAYLOAD_TYPES.get(expected_type)
|
||||
if expected_python_type is None:
|
||||
continue
|
||||
if expected_type == "integer" and isinstance(payload[key], bool):
|
||||
raise ValueError(f"`{key}` must be of type {expected_type}.")
|
||||
if not isinstance(payload[key], expected_python_type):
|
||||
raise ValueError(f"`{key}` must be of type {expected_type}.")
|
||||
return matched_command
|
||||
|
||||
|
||||
def execute_device_command(*, farm_device, device_code, command, payload=None):
|
||||
validate_device_command(farm_device, command, payload or {}, device_code=device_code)
|
||||
return {
|
||||
"physical_device_uuid": farm_device.physical_device_uuid,
|
||||
"device_code": device_code,
|
||||
"command": command,
|
||||
"status": "queued",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||
|
||||
from farm_hub.models import FarmHub, FarmType
|
||||
|
||||
from .models import DeviceCatalog, SensorExternalRequestLog
|
||||
from .views import DeviceCommandView, DeviceDetailView, DeviceLatestPayloadView, DeviceSummaryView
|
||||
|
||||
|
||||
class DeviceHubGenericViewsTests(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username="device-user",
|
||||
password="secret123",
|
||||
email="device@example.com",
|
||||
phone_number="09120001000",
|
||||
)
|
||||
self.farm_type = FarmType.objects.create(name="گلخانه ای")
|
||||
self.farm = FarmHub.objects.create(
|
||||
owner=self.user,
|
||||
farm_type=self.farm_type,
|
||||
name="Device Farm",
|
||||
)
|
||||
self.catalog = DeviceCatalog.objects.create(
|
||||
code="soil_sensor_v2",
|
||||
name="Soil Sensor V2",
|
||||
device_communication_type=DeviceCatalog.OUTPUT_ONLY,
|
||||
returned_data_fields=["soil_moisture", "soil_temperature"],
|
||||
payload_mapping={
|
||||
"soil_moisture": ["moisture", "soil_moisture"],
|
||||
"soil_temperature": ["temperature", "soil_temperature"],
|
||||
},
|
||||
display_schema={
|
||||
"fields": [
|
||||
{"id": "soil_moisture", "label": "رطوبت خاک", "unit": "%", "ideal_min": 40, "ideal_max": 70},
|
||||
{"id": "soil_temperature", "label": "دمای خاک", "unit": "°C", "ideal_min": 18, "ideal_max": 30},
|
||||
]
|
||||
},
|
||||
supported_widgets=["values_list", "comparison_chart", "radar_chart"],
|
||||
)
|
||||
self.device = self.farm.sensors.create(
|
||||
name="Soil Device 1",
|
||||
sensor_catalog=self.catalog,
|
||||
sensor_type="soil",
|
||||
)
|
||||
SensorExternalRequestLog.objects.create(
|
||||
farm_uuid=self.farm.farm_uuid,
|
||||
sensor_catalog_uuid=self.catalog.uuid,
|
||||
physical_device_uuid=self.device.physical_device_uuid,
|
||||
payload={"moisture": 52.4, "temperature": 23.1},
|
||||
)
|
||||
|
||||
def test_device_detail_view_returns_generic_payload(self):
|
||||
request = self.factory.get(
|
||||
f"/api/device-hub/devices/{self.device.physical_device_uuid}/",
|
||||
{"device_code": self.catalog.code},
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = DeviceDetailView.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_catalog"]["code"], 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/",
|
||||
{"device_code": self.catalog.code},
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = DeviceLatestPayloadView.as_view()(request, physical_device_uuid=self.device.physical_device_uuid)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["data"]["normalized_payload"]["soil_moisture"], 52.4)
|
||||
self.assertEqual(response.data["data"]["readings"]["soil_temperature"], 23.1)
|
||||
|
||||
def test_device_summary_view_returns_supported_widgets(self):
|
||||
request = self.factory.get(
|
||||
f"/api/device-hub/devices/{self.device.physical_device_uuid}/summary/",
|
||||
{"device_code": self.catalog.code},
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = DeviceSummaryView.as_view()(request, physical_device_uuid=self.device.physical_device_uuid)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("values_list", response.data["data"]["supportedWidgets"])
|
||||
self.assertIn("sensorValuesList", response.data["data"])
|
||||
|
||||
def test_input_only_device_command_view_rejects_input_only_device_code(self):
|
||||
input_catalog = DeviceCatalog.objects.create(
|
||||
code="valve_v1",
|
||||
name="Valve V1",
|
||||
device_communication_type=DeviceCatalog.INPUT_ONLY,
|
||||
commands_schema=[
|
||||
{"command": "open", "label": "Open", "payload_schema": {"duration_seconds": "integer"}},
|
||||
],
|
||||
)
|
||||
input_device = self.farm.sensors.create(
|
||||
name="Valve 1",
|
||||
sensor_catalog=input_catalog,
|
||||
sensor_type="valve",
|
||||
)
|
||||
request = self.factory.post(
|
||||
f"/api/device-hub/devices/{input_device.physical_device_uuid}/commands/",
|
||||
{"device_code": input_catalog.code, "command": "open", "payload": {"duration_seconds": 120}},
|
||||
format="json",
|
||||
)
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = DeviceCommandView.as_view()(request, physical_device_uuid=input_device.physical_device_uuid)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("device_code", response.data)
|
||||
+11
-3
@@ -1,11 +1,19 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import Sensor7In1SummaryView, SensorCatalogListView, SensorExternalAPIView, SensorExternalRequestLogListAPIView
|
||||
from .views import DeviceCatalogListView, 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>/", 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("summary/", Sensor7In1SummaryView.as_view(), name="sensor-7-in-1-summary"),
|
||||
path("", SensorCatalogListView.as_view(), name="sensor-catalog-list"),
|
||||
path("", DeviceCatalogListView.as_view(), name="sensor-catalog-list"),
|
||||
path("external/", SensorExternalAPIView.as_view(), name="sensor-external-api"),
|
||||
path("external/logs/", SensorExternalRequestLogListAPIView.as_view(), name="sensor-external-api-log-list"),
|
||||
]
|
||||
|
||||
|
||||
+154
-9
@@ -11,18 +11,163 @@ from notifications.serializers import FarmNotificationSerializer
|
||||
from soil.serializers import SoilComparisonChartSerializer, SoilRadarChartSerializer
|
||||
|
||||
from .authentication import SensorExternalAPIKeyAuthentication
|
||||
from .sensor_serializers import Sensor7In1SummarySerializer, SensorComparisonChartQuerySerializer, SensorComparisonChartResponseSerializer, SensorRadarChartQuerySerializer, SensorRadarChartResponseSerializer, SensorValuesListQuerySerializer, SensorValuesListResponseSerializer
|
||||
from .serializers import SensorCatalogSerializer, SensorExternalRequestLogQuerySerializer, SensorExternalRequestLogSerializer, SensorExternalRequestSerializer
|
||||
from .services import FarmDataForwardError, create_sensor_external_notification, forward_sensor_payload_to_farm_data, get_farm_sensor_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
|
||||
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 .services import 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
|
||||
|
||||
|
||||
class SensorCatalogListView(APIView):
|
||||
class DeviceCatalogListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(tags=["Sensor Catalog"], responses={200: code_response("SensorCatalogListResponse", data=SensorCatalogSerializer(many=True))})
|
||||
@extend_schema(tags=["Sensor Catalog"], responses={200: code_response("DeviceCatalogListResponse", data=DeviceCatalogSerializer(many=True))})
|
||||
def get(self, request):
|
||||
from .models import SensorCatalog
|
||||
return Response({"code": 200, "msg": "success", "data": SensorCatalogSerializer(SensorCatalog.objects.order_by("code"), many=True).data}, status=status.HTTP_200_OK)
|
||||
from .models import DeviceCatalog
|
||||
return Response({"code": 200, "msg": "success", "data": DeviceCatalogSerializer(DeviceCatalog.objects.order_by("code"), many=True).data}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DeviceBaseView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_farm_device(self, request, physical_device_uuid):
|
||||
farm_device = get_farm_device_by_physical_uuid(physical_device_uuid=physical_device_uuid, owner=request.user)
|
||||
if farm_device is None:
|
||||
raise serializers.ValidationError({"physical_device_uuid": ["Device not found."]})
|
||||
return farm_device
|
||||
|
||||
def get_device_code(self, request):
|
||||
serializer = DeviceCodeQuerySerializer(data=request.query_params)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer.validated_data["device_code"]
|
||||
|
||||
|
||||
class DeviceDetailView(DeviceBaseView):
|
||||
@extend_schema(tags=["Device Hub"], parameters=[OpenApiParameter(name="device_code", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=True)], responses={200: code_response("DeviceDetailResponse", data=DeviceDetailSerializer())})
|
||||
def get(self, request, physical_device_uuid):
|
||||
farm_device = self.get_farm_device(request, physical_device_uuid)
|
||||
device_code = self.get_device_code(request)
|
||||
validate_output_device_catalog(farm_device=farm_device, device_code=device_code)
|
||||
latest_payload = build_device_latest_payload(farm_device, device_code=device_code)
|
||||
serializer = DeviceDetailSerializer(farm_device, context={"latest_log": type("LatestLog", (), {"created_at": latest_payload["created_at"]})() if latest_payload["created_at"] else None})
|
||||
return Response({"code": 200, "msg": "success", "data": serializer.data}, 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):
|
||||
farm_device = self.get_farm_device(request, physical_device_uuid)
|
||||
device_code = self.get_device_code(request)
|
||||
try:
|
||||
data = build_device_latest_payload(farm_device, device_code=device_code)
|
||||
except ValueError as exc:
|
||||
raise serializers.ValidationError({"device_code": [str(exc)]}) from exc
|
||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DeviceSummaryView(DeviceBaseView):
|
||||
@extend_schema(tags=["Device Hub"], parameters=[OpenApiParameter(name="device_code", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=True)], responses={200: code_response("DeviceSummaryResponse", data=DeviceSummarySerializer())})
|
||||
def get(self, request, physical_device_uuid):
|
||||
farm_device = self.get_farm_device(request, physical_device_uuid)
|
||||
device_code = self.get_device_code(request)
|
||||
try:
|
||||
data = build_device_summary(farm_device, device_code=device_code)
|
||||
except ValueError as exc:
|
||||
raise serializers.ValidationError({"device_code": [str(exc)]}) from exc
|
||||
return Response({"code": 200, "msg": "success", "data": data}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DeviceComparisonChartView(DeviceBaseView):
|
||||
@extend_schema(tags=["Device Hub"], parameters=[OpenApiParameter(name="device_code", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=True), OpenApiParameter(name="range", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=False, description="Chart range, supported values: 7d, 30d. Defaults to 7d.")], responses={200: SensorComparisonChartResponseSerializer})
|
||||
def get(self, request, physical_device_uuid):
|
||||
serializer = DeviceRangeQuerySerializer(data={"device_code": request.query_params.get("device_code"), "range": request.query_params.get("range", "7d")})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
farm_device = self.get_farm_device(request, physical_device_uuid)
|
||||
try:
|
||||
data = build_device_comparison_chart(farm_device, serializer.validated_data["range"], device_code=serializer.validated_data["device_code"])
|
||||
except ValueError as exc:
|
||||
raise serializers.ValidationError({"device_code": [str(exc)]}) from exc
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DeviceValuesListView(DeviceBaseView):
|
||||
@extend_schema(tags=["Device Hub"], parameters=[OpenApiParameter(name="device_code", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=True), OpenApiParameter(name="range", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=False, description="Values list range, supported values: 1h, 24h, 7d. Defaults to 7d.")], responses={200: SensorValuesListResponseSerializer})
|
||||
def get(self, request, physical_device_uuid):
|
||||
serializer = DeviceRangeQuerySerializer(data={"device_code": request.query_params.get("device_code"), "range": request.query_params.get("range", "7d")})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
farm_device = self.get_farm_device(request, physical_device_uuid)
|
||||
try:
|
||||
data = build_device_values_list(farm_device, serializer.validated_data["range"], device_code=serializer.validated_data["device_code"])
|
||||
except ValueError as exc:
|
||||
raise serializers.ValidationError({"device_code": [str(exc)]}) from exc
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DeviceRadarChartView(DeviceBaseView):
|
||||
@extend_schema(tags=["Device Hub"], parameters=[OpenApiParameter(name="device_code", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=True), OpenApiParameter(name="range", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=False, description="Radar chart range, supported values: today, 7d, 30d. Defaults to 7d.")], responses={200: SensorRadarChartResponseSerializer})
|
||||
def get(self, request, physical_device_uuid):
|
||||
serializer = DeviceRangeQuerySerializer(data={"device_code": request.query_params.get("device_code"), "range": request.query_params.get("range", "7d")})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
farm_device = self.get_farm_device(request, physical_device_uuid)
|
||||
try:
|
||||
data = build_device_radar_chart(farm_device, serializer.validated_data["range"], device_code=serializer.validated_data["device_code"])
|
||||
except ValueError as exc:
|
||||
raise serializers.ValidationError({"device_code": [str(exc)]}) from exc
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DeviceLogListView(DeviceBaseView):
|
||||
pagination_class = SensorExternalRequestLogPagination
|
||||
|
||||
@extend_schema(tags=["Device Hub"], parameters=[OpenApiParameter(name="device_code", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, required=True), OpenApiParameter(name="page", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=True), OpenApiParameter(name="page_size", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, required=True)], responses={200: code_response("DeviceLogListResponse", data=SensorExternalRequestLogSerializer(many=True), extra_fields={"count": serializers.IntegerField(), "next": serializers.CharField(allow_null=True), "previous": serializers.CharField(allow_null=True)})})
|
||||
def get(self, request, physical_device_uuid):
|
||||
page = request.query_params.get("page", 1)
|
||||
page_size = request.query_params.get("page_size", 20)
|
||||
device_code = request.query_params.get("device_code")
|
||||
serializer = SensorExternalRequestLogQuerySerializer(
|
||||
data={
|
||||
"farm_uuid": "11111111-1111-1111-1111-111111111111",
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"physical_device_uuid": physical_device_uuid,
|
||||
}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
farm_device = self.get_farm_device(request, physical_device_uuid)
|
||||
try:
|
||||
device_catalog = validate_output_device_catalog(farm_device=farm_device, device_code=device_code)
|
||||
except ValueError as exc:
|
||||
raise serializers.ValidationError({"device_code": [str(exc)]}) from exc
|
||||
queryset = get_sensor_external_request_logs_for_farm(
|
||||
farm_uuid=farm_device.farm.farm_uuid,
|
||||
physical_device_uuid=farm_device.physical_device_uuid,
|
||||
)
|
||||
queryset = queryset.filter(sensor_catalog_uuid=device_catalog.uuid)
|
||||
paginator = self.pagination_class()
|
||||
paginator.page_size = serializer.validated_data["page_size"]
|
||||
page_obj = paginator.paginate_queryset(queryset, request, view=self)
|
||||
farm_device_map = get_farm_device_map_for_logs(logs=page_obj)
|
||||
data = SensorExternalRequestLogSerializer(page_obj, many=True, context={"farm_device_map": farm_device_map}).data
|
||||
return Response({"code": 200, "msg": "success", "count": paginator.page.paginator.count, "next": paginator.get_next_link(), "previous": paginator.get_previous_link(), "data": data}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class DeviceCommandView(DeviceBaseView):
|
||||
@extend_schema(tags=["Device Hub"], request=DeviceCommandRequestSerializer, responses={200: code_response("DeviceCommandResponse", data=DeviceCommandResponseSerializer())})
|
||||
def post(self, request, physical_device_uuid):
|
||||
serializer = DeviceCommandRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
farm_device = self.get_farm_device(request, physical_device_uuid)
|
||||
try:
|
||||
device_catalog = farm_device.get_device_catalog_by_code(serializer.validated_data["device_code"])
|
||||
if device_catalog is None:
|
||||
raise ValueError("Device code is not attached to this farm device.")
|
||||
result = execute_device_command(
|
||||
farm_device=farm_device,
|
||||
device_code=serializer.validated_data["device_code"],
|
||||
command=serializer.validated_data["command"],
|
||||
payload=serializer.validated_data.get("payload"),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise serializers.ValidationError({"device_code": [str(exc)]}) from exc
|
||||
return Response({"code": 200, "msg": "command accepted", "data": result}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class Sensor7In1SummaryView(APIView):
|
||||
@@ -137,6 +282,6 @@ class SensorExternalRequestLogListAPIView(APIView):
|
||||
paginator = self.pagination_class()
|
||||
paginator.page_size = serializer.validated_data["page_size"]
|
||||
page = paginator.paginate_queryset(queryset, request, view=self)
|
||||
farm_sensor_map = get_farm_sensor_map_for_logs(logs=page)
|
||||
data = SensorExternalRequestLogSerializer(page, many=True, context={"farm_sensor_map": farm_sensor_map}).data
|
||||
farm_device_map = get_farm_device_map_for_logs(logs=page)
|
||||
data = SensorExternalRequestLogSerializer(page, many=True, context={"farm_device_map": farm_device_map}).data
|
||||
return Response({"code": 200, "msg": "success", "count": paginator.page.paginator.count, "next": paginator.get_next_link(), "previous": paginator.get_previous_link(), "data": data}, status=status.HTTP_200_OK)
|
||||
|
||||
Reference in New Issue
Block a user