UPDATE
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Sensor7In1Config(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "sensor_7_in_1"
|
||||
verbose_name = "Sensor 7 in 1"
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
AVG_SOIL_MOISTURE = {
|
||||
"id": "avg_soil_moisture",
|
||||
"title": "میانگین رطوبت خاک",
|
||||
"subtitle": "سنسور 7 در 1 خاک",
|
||||
"stats": "45%",
|
||||
"avatarColor": "primary",
|
||||
"avatarIcon": "tabler-droplet",
|
||||
"chipText": "متوسط",
|
||||
"chipColor": "warning",
|
||||
}
|
||||
|
||||
|
||||
SENSOR_VALUES_LIST = {
|
||||
"sensor": {
|
||||
"name": "سنسور 7 در 1 خاک",
|
||||
"physicalDeviceUuid": None,
|
||||
"sensorCatalogCode": "sensor-7-in-1",
|
||||
"updatedAt": None,
|
||||
},
|
||||
"sensors": [
|
||||
{
|
||||
"id": "soil_moisture",
|
||||
"title": "45%",
|
||||
"subtitle": "رطوبت خاک",
|
||||
"trendNumber": 1.5,
|
||||
"trend": "positive",
|
||||
"unit": "%",
|
||||
},
|
||||
{
|
||||
"id": "soil_temperature",
|
||||
"title": "22.5°C",
|
||||
"subtitle": "دمای خاک",
|
||||
"trendNumber": 0.8,
|
||||
"trend": "positive",
|
||||
"unit": "°C",
|
||||
},
|
||||
{
|
||||
"id": "soil_ph",
|
||||
"title": "6.8",
|
||||
"subtitle": "pH خاک",
|
||||
"trendNumber": 0.1,
|
||||
"trend": "positive",
|
||||
"unit": "pH",
|
||||
},
|
||||
{
|
||||
"id": "electrical_conductivity",
|
||||
"title": "1.2 dS/m",
|
||||
"subtitle": "هدایت الکتریکی",
|
||||
"trendNumber": -0.1,
|
||||
"trend": "negative",
|
||||
"unit": "dS/m",
|
||||
},
|
||||
{
|
||||
"id": "nitrogen",
|
||||
"title": "30 mg/kg",
|
||||
"subtitle": "نیتروژن",
|
||||
"trendNumber": 2.0,
|
||||
"trend": "positive",
|
||||
"unit": "mg/kg",
|
||||
},
|
||||
{
|
||||
"id": "phosphorus",
|
||||
"title": "15 mg/kg",
|
||||
"subtitle": "فسفر",
|
||||
"trendNumber": 1.0,
|
||||
"trend": "positive",
|
||||
"unit": "mg/kg",
|
||||
},
|
||||
{
|
||||
"id": "potassium",
|
||||
"title": "20 mg/kg",
|
||||
"subtitle": "پتاسیم",
|
||||
"trendNumber": -1.0,
|
||||
"trend": "negative",
|
||||
"unit": "mg/kg",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
SENSOR_RADAR_CHART = {
|
||||
"labels": ["رطوبت", "دما", "pH", "EC", "نیتروژن", "فسفر", "پتاسیم"],
|
||||
"series": [
|
||||
{"name": "اکنون", "data": [82, 76, 90, 72, 68, 62, 70]},
|
||||
{"name": "هدف", "data": [100, 100, 100, 100, 100, 100, 100]},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
SENSOR_COMPARISON_CHART = {
|
||||
"currentValue": 45,
|
||||
"vsLastWeek": "+4.7%",
|
||||
"vsLastWeekValue": 4.7,
|
||||
"categories": ["08:00", "10:00", "12:00", "14:00", "16:00", "18:00", "20:00"],
|
||||
"series": [
|
||||
{"name": "رطوبت خاک", "data": [42, 44, 45, 47, 46, 45, 45]},
|
||||
{"name": "بازه هدف", "data": [55, 55, 55, 55, 55, 55, 55]},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
ANOMALY_DETECTION_CARD = {
|
||||
"anomalies": [
|
||||
{
|
||||
"sensor": "هدایت الکتریکی",
|
||||
"value": "1.2 dS/m",
|
||||
"expected": "0.8-1.1 dS/m",
|
||||
"deviation": "+0.1 dS/m",
|
||||
"severity": "warning",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
SOIL_MOISTURE_HEATMAP = {
|
||||
"zones": ["سنسور 7 در 1 خاک"],
|
||||
"hours": ["08:00", "10:00", "12:00", "14:00", "16:00", "18:00", "20:00"],
|
||||
"series": [
|
||||
{
|
||||
"name": "سنسور 7 در 1 خاک",
|
||||
"data": [
|
||||
{"x": "08:00", "y": 42},
|
||||
{"x": "10:00", "y": 44},
|
||||
{"x": "12:00", "y": 45},
|
||||
{"x": "14:00", "y": 47},
|
||||
{"x": "16:00", "y": 46},
|
||||
{"x": "18:00", "y": 45},
|
||||
{"x": "20:00", "y": 45},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
This app is service-based and does not define local database models.
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from soil.serializers import (
|
||||
SoilAnomalyDetectionSerializer,
|
||||
SoilComparisonChartSerializer,
|
||||
SoilKpiSerializer,
|
||||
SoilMoistureHeatmapSerializer,
|
||||
SoilRadarChartSerializer,
|
||||
)
|
||||
|
||||
|
||||
class Sensor7In1MetaSerializer(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 Sensor7In1ValueSerializer(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 Sensor7In1ValuesListSerializer(serializers.Serializer):
|
||||
sensor = Sensor7In1MetaSerializer(required=False)
|
||||
sensors = Sensor7In1ValueSerializer(many=True, required=False)
|
||||
|
||||
|
||||
class Sensor7In1SummarySerializer(serializers.Serializer):
|
||||
sensor = Sensor7In1MetaSerializer(required=False)
|
||||
sensorValuesList = Sensor7In1ValuesListSerializer(required=False)
|
||||
avgSoilMoisture = SoilKpiSerializer(required=False)
|
||||
sensorRadarChart = SoilRadarChartSerializer(required=False)
|
||||
sensorComparisonChart = SoilComparisonChartSerializer(required=False)
|
||||
anomalyDetectionCard = SoilAnomalyDetectionSerializer(required=False)
|
||||
soilMoistureHeatmap = SoilMoistureHeatmapSerializer(required=False)
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from sensor_external_api.services import get_farm_sensor_map_for_logs, get_sensor_external_request_logs_for_farm
|
||||
|
||||
from .mock_data import (
|
||||
ANOMALY_DETECTION_CARD,
|
||||
AVG_SOIL_MOISTURE,
|
||||
SENSOR_COMPARISON_CHART,
|
||||
SENSOR_RADAR_CHART,
|
||||
SENSOR_VALUES_LIST,
|
||||
SOIL_MOISTURE_HEATMAP,
|
||||
)
|
||||
|
||||
|
||||
SENSOR_FIELDS = [
|
||||
{
|
||||
"id": "soil_moisture",
|
||||
"label": "رطوبت خاک",
|
||||
"unit": "%",
|
||||
"payload_keys": ("soil_moisture", "soilMoisture", "moisture"),
|
||||
"ideal_min": 45.0,
|
||||
"ideal_max": 65.0,
|
||||
"radar_label": "رطوبت",
|
||||
},
|
||||
{
|
||||
"id": "soil_temperature",
|
||||
"label": "دمای خاک",
|
||||
"unit": "°C",
|
||||
"payload_keys": ("soil_temperature", "soilTemperature", "temperature"),
|
||||
"ideal_min": 18.0,
|
||||
"ideal_max": 28.0,
|
||||
"radar_label": "دما",
|
||||
},
|
||||
{
|
||||
"id": "soil_ph",
|
||||
"label": "pH خاک",
|
||||
"unit": "pH",
|
||||
"payload_keys": ("soil_ph", "soilPh", "ph"),
|
||||
"ideal_min": 6.0,
|
||||
"ideal_max": 7.5,
|
||||
"radar_label": "pH",
|
||||
},
|
||||
{
|
||||
"id": "electrical_conductivity",
|
||||
"label": "هدایت الکتریکی",
|
||||
"unit": "dS/m",
|
||||
"payload_keys": ("electrical_conductivity", "electricalConductivity", "ec"),
|
||||
"ideal_min": 0.8,
|
||||
"ideal_max": 1.8,
|
||||
"radar_label": "EC",
|
||||
},
|
||||
{
|
||||
"id": "nitrogen",
|
||||
"label": "نیتروژن",
|
||||
"unit": "mg/kg",
|
||||
"payload_keys": ("nitrogen", "n"),
|
||||
"ideal_min": 20.0,
|
||||
"ideal_max": 40.0,
|
||||
"radar_label": "نیتروژن",
|
||||
},
|
||||
{
|
||||
"id": "phosphorus",
|
||||
"label": "فسفر",
|
||||
"unit": "mg/kg",
|
||||
"payload_keys": ("phosphorus", "p"),
|
||||
"ideal_min": 10.0,
|
||||
"ideal_max": 25.0,
|
||||
"radar_label": "فسفر",
|
||||
},
|
||||
{
|
||||
"id": "potassium",
|
||||
"label": "پتاسیم",
|
||||
"unit": "mg/kg",
|
||||
"payload_keys": ("potassium", "k"),
|
||||
"ideal_min": 15.0,
|
||||
"ideal_max": 35.0,
|
||||
"radar_label": "پتاسیم",
|
||||
},
|
||||
]
|
||||
|
||||
MIN_REQUIRED_SENSOR_FIELDS = 4
|
||||
MAX_HISTORY_ITEMS = 20
|
||||
MAX_CHART_POINTS = 7
|
||||
|
||||
|
||||
def _to_float(value):
|
||||
if value is None or isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _extract_payload(payload):
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
|
||||
if isinstance(payload.get("payload"), dict):
|
||||
payload = payload["payload"]
|
||||
|
||||
if isinstance(payload.get("data"), dict):
|
||||
nested = payload["data"]
|
||||
if any(any(key in nested for key in field["payload_keys"]) for field in SENSOR_FIELDS):
|
||||
payload = nested
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def _extract_readings(payload):
|
||||
payload = _extract_payload(payload)
|
||||
readings = {}
|
||||
for field in SENSOR_FIELDS:
|
||||
for key in field["payload_keys"]:
|
||||
value = _to_float(payload.get(key))
|
||||
if value is not None:
|
||||
readings[field["id"]] = value
|
||||
break
|
||||
return readings
|
||||
|
||||
|
||||
def _format_number(value):
|
||||
if value is None:
|
||||
return ""
|
||||
if float(value).is_integer():
|
||||
return str(int(value))
|
||||
return f"{value:.1f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def _format_value(value, unit):
|
||||
number = _format_number(value)
|
||||
if not number:
|
||||
return number
|
||||
if unit in {"", "pH"}:
|
||||
return number
|
||||
if unit in {"%", "°C"}:
|
||||
return f"{number}{unit}"
|
||||
return f"{number} {unit}"
|
||||
|
||||
|
||||
def _format_range(field):
|
||||
lower = _format_number(field["ideal_min"])
|
||||
upper = _format_number(field["ideal_max"])
|
||||
unit = field["unit"]
|
||||
if unit in {"", "pH"}:
|
||||
return f"{lower}-{upper}"
|
||||
return f"{lower}-{upper} {unit}"
|
||||
|
||||
|
||||
def _get_sensor_context(farm=None):
|
||||
if farm is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
logs_queryset = get_sensor_external_request_logs_for_farm(farm_uuid=farm.farm_uuid)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
candidate_log = None
|
||||
candidate_readings = {}
|
||||
for log in logs_queryset[:MAX_HISTORY_ITEMS]:
|
||||
readings = _extract_readings(log.payload)
|
||||
if len(readings) >= MIN_REQUIRED_SENSOR_FIELDS:
|
||||
candidate_log = log
|
||||
candidate_readings = readings
|
||||
break
|
||||
|
||||
if candidate_log is None:
|
||||
return None
|
||||
|
||||
history = []
|
||||
for log in logs_queryset.filter(physical_device_uuid=candidate_log.physical_device_uuid)[:MAX_HISTORY_ITEMS]:
|
||||
readings = _extract_readings(log.payload)
|
||||
if readings:
|
||||
history.append((log, readings))
|
||||
|
||||
if not history:
|
||||
history = [(candidate_log, candidate_readings)]
|
||||
|
||||
farm_sensor_map = get_farm_sensor_map_for_logs(logs=[candidate_log])
|
||||
farm_sensor = farm_sensor_map.get(
|
||||
(candidate_log.farm_uuid, candidate_log.sensor_catalog_uuid, candidate_log.physical_device_uuid)
|
||||
)
|
||||
|
||||
return {
|
||||
"farm_sensor": farm_sensor,
|
||||
"latest_log": history[0][0],
|
||||
"latest_readings": history[0][1],
|
||||
"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")
|
||||
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
|
||||
|
||||
return sensor
|
||||
|
||||
|
||||
def _calculate_status_chip(value):
|
||||
if value is None:
|
||||
return ("نامشخص", "secondary", "secondary")
|
||||
if value >= 60:
|
||||
return ("بهینه", "success", "primary")
|
||||
if value >= 45:
|
||||
return ("متوسط", "warning", "warning")
|
||||
return ("کم", "error", "error")
|
||||
|
||||
|
||||
def get_sensor_7_in_1_values_list_data(farm=None, context=None):
|
||||
data = deepcopy(SENSOR_VALUES_LIST)
|
||||
context = _get_sensor_context(farm) if context is None else context
|
||||
data["sensor"] = _build_sensor_meta(context, data["sensor"])
|
||||
if not context:
|
||||
return data
|
||||
|
||||
latest_readings = context["latest_readings"]
|
||||
previous_readings = context["previous_readings"]
|
||||
sensors = []
|
||||
|
||||
for field in SENSOR_FIELDS:
|
||||
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)
|
||||
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"],
|
||||
}
|
||||
)
|
||||
|
||||
if sensors:
|
||||
data["sensors"] = sensors
|
||||
return data
|
||||
|
||||
|
||||
def get_sensor_7_in_1_avg_soil_moisture_data(farm=None, context=None):
|
||||
data = deepcopy(AVG_SOIL_MOISTURE)
|
||||
context = _get_sensor_context(farm) if context is None else context
|
||||
if not context:
|
||||
return data
|
||||
|
||||
moisture = context["latest_readings"].get("soil_moisture")
|
||||
if moisture is None:
|
||||
return data
|
||||
|
||||
chip_text, chip_color, avatar_color = _calculate_status_chip(moisture)
|
||||
data["stats"] = _format_value(moisture, "%")
|
||||
data["chipText"] = chip_text
|
||||
data["chipColor"] = chip_color
|
||||
data["avatarColor"] = avatar_color
|
||||
return data
|
||||
|
||||
|
||||
def _score_field(value, field):
|
||||
min_value = field["ideal_min"]
|
||||
max_value = field["ideal_max"]
|
||||
midpoint = (min_value + max_value) / 2
|
||||
half_span = max((max_value - min_value) / 2, 0.1)
|
||||
distance = abs(value - midpoint)
|
||||
|
||||
if min_value <= value <= max_value:
|
||||
return round(max(80.0, 100.0 - ((distance / half_span) * 20.0)), 1)
|
||||
|
||||
overflow = max(0.0, distance - half_span)
|
||||
return round(max(0.0, 80.0 - ((overflow / half_span) * 80.0)), 1)
|
||||
|
||||
|
||||
def get_sensor_7_in_1_radar_chart_data(farm=None, context=None):
|
||||
data = deepcopy(SENSOR_RADAR_CHART)
|
||||
context = _get_sensor_context(farm) if context is None else context
|
||||
if not context:
|
||||
return data
|
||||
|
||||
latest_readings = context["latest_readings"]
|
||||
scores = []
|
||||
labels = []
|
||||
for field in SENSOR_FIELDS:
|
||||
value = latest_readings.get(field["id"])
|
||||
if value is None:
|
||||
continue
|
||||
labels.append(field["radar_label"])
|
||||
scores.append(_score_field(value, field))
|
||||
|
||||
if labels:
|
||||
data["labels"] = labels
|
||||
data["series"] = [
|
||||
{"name": "اکنون", "data": scores},
|
||||
{"name": "هدف", "data": [100.0] * len(labels)},
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
def get_sensor_7_in_1_comparison_chart_data(farm=None, context=None):
|
||||
data = deepcopy(SENSOR_COMPARISON_CHART)
|
||||
context = _get_sensor_context(farm) if context is None else context
|
||||
if not context:
|
||||
return data
|
||||
|
||||
history = list(reversed(context["history"][:MAX_CHART_POINTS]))
|
||||
moisture_points = [
|
||||
(log.created_at.strftime("%m/%d %H:%M"), readings.get("soil_moisture"))
|
||||
for log, readings in history
|
||||
if readings.get("soil_moisture") is not None
|
||||
]
|
||||
if not moisture_points:
|
||||
return data
|
||||
|
||||
categories = [item[0] for item in moisture_points]
|
||||
values = [round(item[1], 2) for item in moisture_points]
|
||||
current_value = values[-1]
|
||||
baseline_value = values[0] if len(values) > 1 else 55.0
|
||||
percent_change = 0.0
|
||||
if baseline_value:
|
||||
percent_change = ((current_value - baseline_value) / baseline_value) * 100
|
||||
|
||||
data["currentValue"] = round(current_value, 2)
|
||||
data["vsLastWeekValue"] = round(percent_change, 2)
|
||||
data["vsLastWeek"] = f"{percent_change:+.1f}%"
|
||||
data["categories"] = categories
|
||||
data["series"] = [
|
||||
{"name": "رطوبت خاک", "data": values},
|
||||
{"name": "بازه هدف", "data": [55.0] * len(values)},
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
def _build_anomaly_item(field, value):
|
||||
lower = field["ideal_min"]
|
||||
upper = field["ideal_max"]
|
||||
if lower <= value <= upper:
|
||||
return None
|
||||
|
||||
deviation = value - upper if value > upper else value - lower
|
||||
severity = "warning"
|
||||
span = max(upper - lower, 0.1)
|
||||
if abs(deviation) >= span * 0.5:
|
||||
severity = "error"
|
||||
|
||||
sign = "+" if deviation > 0 else ""
|
||||
return {
|
||||
"sensor": field["label"],
|
||||
"value": _format_value(value, field["unit"]),
|
||||
"expected": _format_range(field),
|
||||
"deviation": f"{sign}{_format_value(deviation, field['unit'])}",
|
||||
"severity": severity,
|
||||
}
|
||||
|
||||
|
||||
def get_sensor_7_in_1_anomaly_detection_card_data(farm=None, context=None):
|
||||
data = deepcopy(ANOMALY_DETECTION_CARD)
|
||||
context = _get_sensor_context(farm) if context is None else context
|
||||
if not context:
|
||||
return data
|
||||
|
||||
anomalies = []
|
||||
for field in SENSOR_FIELDS:
|
||||
value = context["latest_readings"].get(field["id"])
|
||||
if value is None:
|
||||
continue
|
||||
anomaly = _build_anomaly_item(field, value)
|
||||
if anomaly is not None:
|
||||
anomalies.append(anomaly)
|
||||
|
||||
if anomalies:
|
||||
data["anomalies"] = anomalies
|
||||
else:
|
||||
data["anomalies"] = [
|
||||
{
|
||||
"sensor": "سنسور 7 در 1 خاک",
|
||||
"value": "نرمال",
|
||||
"expected": "تمام شاخصها در بازه مجاز هستند",
|
||||
"deviation": "0",
|
||||
"severity": "success",
|
||||
}
|
||||
]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_sensor_7_in_1_soil_moisture_heatmap_data(farm=None, context=None):
|
||||
data = deepcopy(SOIL_MOISTURE_HEATMAP)
|
||||
context = _get_sensor_context(farm) if context is None else context
|
||||
if not context:
|
||||
return data
|
||||
|
||||
history = list(reversed(context["history"][:MAX_CHART_POINTS]))
|
||||
chart_points = [
|
||||
{"x": log.created_at.strftime("%H:%M"), "y": round(readings.get("soil_moisture"), 2)}
|
||||
for log, readings in history
|
||||
if readings.get("soil_moisture") is not 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
|
||||
|
||||
data["zones"] = [sensor_name]
|
||||
data["hours"] = [point["x"] for point in chart_points]
|
||||
data["series"] = [{"name": sensor_name, "data": chart_points}]
|
||||
return data
|
||||
|
||||
|
||||
def get_sensor_7_in_1_summary_data(farm=None):
|
||||
context = _get_sensor_context(farm)
|
||||
values_list = get_sensor_7_in_1_values_list_data(farm, context=context)
|
||||
return {
|
||||
"sensor": values_list["sensor"],
|
||||
"sensorValuesList": values_list,
|
||||
"avgSoilMoisture": get_sensor_7_in_1_avg_soil_moisture_data(farm, context=context),
|
||||
"sensorRadarChart": get_sensor_7_in_1_radar_chart_data(farm, context=context),
|
||||
"sensorComparisonChart": get_sensor_7_in_1_comparison_chart_data(farm, context=context),
|
||||
"anomalyDetectionCard": get_sensor_7_in_1_anomaly_detection_card_data(farm, context=context),
|
||||
"soilMoistureHeatmap": get_sensor_7_in_1_soil_moisture_heatmap_data(farm, context=context),
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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, FarmSensor, FarmType
|
||||
from sensor_catalog.models import SensorCatalog
|
||||
from sensor_external_api.models import SensorExternalRequestLog
|
||||
|
||||
from dashboard.services import get_farm_dashboard_cards
|
||||
|
||||
from .services import get_sensor_7_in_1_summary_data
|
||||
from .views import Sensor7In1SummaryView
|
||||
|
||||
|
||||
class Sensor7In1BaseTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.factory = APIRequestFactory()
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username="sensor-7-in-1-user",
|
||||
password="secret123",
|
||||
email="sensor7@example.com",
|
||||
phone_number="09120000017",
|
||||
)
|
||||
self.farm_type = FarmType.objects.create(name="مزرعه سنسور 7 در 1")
|
||||
self.farm = FarmHub.objects.create(
|
||||
owner=self.user,
|
||||
farm_type=self.farm_type,
|
||||
name="Farm Sensor 7 in 1",
|
||||
farm_uuid="11111111-1111-1111-1111-111111111111",
|
||||
)
|
||||
self.sensor_catalog = SensorCatalog.objects.create(
|
||||
code="sensor-7-in-1",
|
||||
name="7 in 1 Soil Sensor",
|
||||
returned_data_fields=[
|
||||
"soil_moisture",
|
||||
"soil_temperature",
|
||||
"soil_ph",
|
||||
"electrical_conductivity",
|
||||
"nitrogen",
|
||||
"phosphorus",
|
||||
"potassium",
|
||||
],
|
||||
)
|
||||
self.sensor = FarmSensor.objects.create(
|
||||
farm=self.farm,
|
||||
sensor_catalog=self.sensor_catalog,
|
||||
physical_device_uuid="33333333-3333-3333-3333-333333333333",
|
||||
name="Soil Sensor 7-in-1",
|
||||
sensor_type="soil_7_in_1",
|
||||
)
|
||||
SensorExternalRequestLog.objects.create(
|
||||
farm_uuid=self.farm.farm_uuid,
|
||||
sensor_catalog_uuid=self.sensor_catalog.uuid,
|
||||
physical_device_uuid=self.sensor.physical_device_uuid,
|
||||
payload={
|
||||
"soil_moisture": 41.0,
|
||||
"soil_temperature": 21.0,
|
||||
"soil_ph": 6.5,
|
||||
"electrical_conductivity": 1.0,
|
||||
"nitrogen": 28.0,
|
||||
"phosphorus": 14.0,
|
||||
"potassium": 19.0,
|
||||
},
|
||||
)
|
||||
self.latest_log = SensorExternalRequestLog.objects.create(
|
||||
farm_uuid=self.farm.farm_uuid,
|
||||
sensor_catalog_uuid=self.sensor_catalog.uuid,
|
||||
physical_device_uuid=self.sensor.physical_device_uuid,
|
||||
payload={
|
||||
"soil_moisture": 48.5,
|
||||
"soil_temperature": 23.2,
|
||||
"soil_ph": 6.8,
|
||||
"electrical_conductivity": 1.4,
|
||||
"nitrogen": 31.0,
|
||||
"phosphorus": 16.0,
|
||||
"potassium": 24.0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class Sensor7In1ServiceTests(Sensor7In1BaseTestCase):
|
||||
def test_summary_returns_latest_specific_sensor_data(self):
|
||||
data = get_sensor_7_in_1_summary_data(self.farm)
|
||||
|
||||
self.assertEqual(data["sensor"]["name"], "Soil Sensor 7-in-1")
|
||||
self.assertEqual(data["sensor"]["physicalDeviceUuid"], str(self.sensor.physical_device_uuid))
|
||||
self.assertEqual(data["sensorValuesList"]["sensors"][0]["id"], "soil_moisture")
|
||||
self.assertEqual(data["avgSoilMoisture"]["stats"], "48.5%")
|
||||
self.assertEqual(data["sensorComparisonChart"]["currentValue"], 48.5)
|
||||
self.assertEqual(data["soilMoistureHeatmap"]["series"][0]["name"], "Soil Sensor 7-in-1")
|
||||
|
||||
def test_dashboard_cards_use_sensor_service_outputs(self):
|
||||
cards = get_farm_dashboard_cards(self.farm)
|
||||
|
||||
self.assertEqual(cards["sensorValuesList"]["sensor"]["physicalDeviceUuid"], str(self.sensor.physical_device_uuid))
|
||||
self.assertEqual(cards["sensorValuesList"]["sensors"][0]["title"], "48.5%")
|
||||
self.assertEqual(cards["sensorRadarChart"]["series"][0]["name"], "اکنون")
|
||||
self.assertEqual(cards["soilMoistureHeatmap"]["series"][0]["name"], "Soil Sensor 7-in-1")
|
||||
self.assertEqual(cards["farmOverviewKpis"]["kpis"][2]["stats"], "48.5%")
|
||||
|
||||
|
||||
class Sensor7In1ViewTests(Sensor7In1BaseTestCase):
|
||||
def test_summary_view_returns_sensor_cards(self):
|
||||
request = self.factory.get(f"/api/sensor-7-in-1/summary/?farm_uuid={self.farm.farm_uuid}")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = Sensor7In1SummaryView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data["code"], 200)
|
||||
self.assertEqual(response.data["data"]["sensor"]["sensorCatalogCode"], "sensor-7-in-1")
|
||||
|
||||
def test_summary_view_requires_farm_uuid(self):
|
||||
request = self.factory.get("/api/sensor-7-in-1/summary/")
|
||||
force_authenticate(request, user=self.user)
|
||||
|
||||
response = Sensor7In1SummaryView.as_view()(request)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.data["farm_uuid"][0], "This field is required.")
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import Sensor7In1SummaryView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("summary/", Sensor7In1SummaryView.as_view(), name="sensor-7-in-1-summary"),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
|
||||
from config.swagger import code_response
|
||||
from farm_hub.models import FarmHub
|
||||
|
||||
from .serializers import Sensor7In1SummarySerializer
|
||||
from .services import get_sensor_7_in_1_summary_data
|
||||
|
||||
|
||||
class Sensor7In1SummaryView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
required_feature_code = "sensor-7-in-1"
|
||||
|
||||
@staticmethod
|
||||
def _get_farm(request):
|
||||
farm_uuid = request.query_params.get("farm_uuid")
|
||||
if not farm_uuid:
|
||||
raise serializers.ValidationError({"farm_uuid": ["This field is required."]})
|
||||
try:
|
||||
return FarmHub.objects.get(farm_uuid=farm_uuid, owner=request.user)
|
||||
except FarmHub.DoesNotExist as exc:
|
||||
raise serializers.ValidationError({"farm_uuid": ["Farm not found."]}) from exc
|
||||
|
||||
@extend_schema(
|
||||
tags=["Sensor 7 in 1"],
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="farm_uuid",
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.QUERY,
|
||||
required=True,
|
||||
default="11111111-1111-1111-1111-111111111111",
|
||||
)
|
||||
],
|
||||
responses={200: code_response("Sensor7In1SummaryResponse", data=Sensor7In1SummarySerializer())},
|
||||
)
|
||||
def get(self, request):
|
||||
farm = self._get_farm(request)
|
||||
return Response(
|
||||
{"code": 200, "msg": "OK", "data": get_sensor_7_in_1_summary_data(farm)},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user