This commit is contained in:
2026-04-11 03:54:15 +03:30
parent 883573004c
commit 36d6b05a7f
68 changed files with 3487 additions and 841 deletions
+1
View File
@@ -0,0 +1 @@
+7
View File
@@ -0,0 +1,7 @@
from django.apps import AppConfig
class SoilConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "soil"
verbose_name = "Soil"
+83
View File
@@ -0,0 +1,83 @@
AVG_SOIL_MOISTURE = {
"id": "avg_soil_moisture",
"title": "میانگین رطوبت خاک",
"subtitle": "کل مزرعه",
"stats": "65%",
"avatarColor": "primary",
"avatarIcon": "tabler-plant-2",
"chipText": "بهینه",
"chipColor": "success",
}
SENSOR_RADAR_CHART = {
"labels": ["دما", "رطوبت", "pH", "هدایت الکتریکی", "نور", "باد"],
"series": [
{"name": "امروز", "data": [75, 65, 80, 70, 85, 60]},
{"name": "ایده آل", "data": [80, 70, 75, 75, 90, 50]},
],
}
SENSOR_COMPARISON_CHART = {
"currentValue": 48,
"vsLastWeek": "+5%",
"vsLastWeekValue": 5,
"categories": ["دوشنبه", "سه شنبه", "چهارشنبه", "پنج شنبه", "جمعه", "شنبه", "یکشنبه"],
"series": [
{"name": "امروز", "data": [42, 45, 48, 52, 50, 48, 46]},
{"name": "هفته قبل", "data": [38, 40, 42, 45, 43, 40, 38]},
],
}
ANOMALY_DETECTION_CARD = {
"anomalies": [
{
"sensor": "رطوبت خاک زون 3",
"value": "38%",
"expected": "45-65%",
"deviation": "-12%",
"severity": "warning",
},
{
"sensor": "pH بخش 2",
"value": "5.2",
"expected": "6.0-7.0",
"deviation": "-0.8",
"severity": "error",
},
]
}
SOIL_MOISTURE_HEATMAP = {
"zones": ["زون 1", "زون 2", "زون 3", "زون 4", "زون 5", "زون 6", "زون 7"],
"hours": ["6 ص", "8 ص", "10 ص", "12 ظ", "14 ع", "16 ع", "18 ع"],
"series": [
{
"name": "زون 1",
"data": [
{"x": "6 ص", "y": 52},
{"x": "8 ص", "y": 48},
{"x": "10 ص", "y": 55},
{"x": "12 ظ", "y": 60},
{"x": "14 ع", "y": 58},
{"x": "16 ع", "y": 54},
{"x": "18 ع", "y": 50},
],
},
{
"name": "زون 2",
"data": [
{"x": "6 ص", "y": 45},
{"x": "8 ص", "y": 42},
{"x": "10 ص", "y": 48},
{"x": "12 ظ", "y": 52},
{"x": "14 ع", "y": 50},
{"x": "16 ع", "y": 47},
{"x": "18 ع", "y": 44},
],
},
],
}
+1
View File
@@ -0,0 +1 @@
from django.db import models
+66
View File
@@ -0,0 +1,66 @@
from rest_framework import serializers
class SoilKpiSerializer(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)
stats = serializers.CharField(required=False, allow_blank=True)
avatarColor = serializers.CharField(required=False, allow_blank=True)
avatarIcon = serializers.CharField(required=False, allow_blank=True)
chipText = serializers.CharField(required=False, allow_blank=True)
chipColor = serializers.CharField(required=False, allow_blank=True)
class SoilRadarSeriesSerializer(serializers.Serializer):
name = serializers.CharField(required=False, allow_blank=True)
data = serializers.ListField(child=serializers.FloatField(), required=False)
class SoilRadarChartSerializer(serializers.Serializer):
labels = serializers.ListField(child=serializers.CharField(), required=False)
series = SoilRadarSeriesSerializer(many=True, required=False)
class SoilComparisonChartSerializer(serializers.Serializer):
currentValue = serializers.FloatField(required=False)
vsLastWeek = serializers.CharField(required=False, allow_blank=True)
vsLastWeekValue = serializers.FloatField(required=False)
categories = serializers.ListField(child=serializers.CharField(), required=False)
series = SoilRadarSeriesSerializer(many=True, required=False)
class SoilAnomalyItemSerializer(serializers.Serializer):
sensor = serializers.CharField(required=False, allow_blank=True)
value = serializers.CharField(required=False, allow_blank=True)
expected = serializers.CharField(required=False, allow_blank=True)
deviation = serializers.CharField(required=False, allow_blank=True)
severity = serializers.CharField(required=False, allow_blank=True)
class SoilAnomalyDetectionSerializer(serializers.Serializer):
anomalies = SoilAnomalyItemSerializer(many=True, required=False)
class SoilHeatmapPointSerializer(serializers.Serializer):
x = serializers.CharField(required=False, allow_blank=True)
y = serializers.FloatField(required=False)
class SoilHeatmapSeriesSerializer(serializers.Serializer):
name = serializers.CharField(required=False, allow_blank=True)
data = SoilHeatmapPointSerializer(many=True, required=False)
class SoilMoistureHeatmapSerializer(serializers.Serializer):
zones = serializers.ListField(child=serializers.CharField(), required=False)
hours = serializers.ListField(child=serializers.CharField(), required=False)
series = SoilHeatmapSeriesSerializer(many=True, required=False)
class SoilSummarySerializer(serializers.Serializer):
avgSoilMoisture = SoilKpiSerializer(required=False)
sensorRadarChart = SoilRadarChartSerializer(required=False)
sensorComparisonChart = SoilComparisonChartSerializer(required=False)
anomalyDetectionCard = SoilAnomalyDetectionSerializer(required=False)
soilMoistureHeatmap = SoilMoistureHeatmapSerializer(required=False)
+84
View File
@@ -0,0 +1,84 @@
from copy import deepcopy
from farm_alerts.models import AnomalyDetection
from .mock_data import (
ANOMALY_DETECTION_CARD,
AVG_SOIL_MOISTURE,
SENSOR_COMPARISON_CHART,
SENSOR_RADAR_CHART,
SOIL_MOISTURE_HEATMAP,
)
def get_avg_soil_moisture_data(farm=None):
data = deepcopy(AVG_SOIL_MOISTURE)
heatmap = get_soil_moisture_heatmap_data(farm)
values = [
point.get("y")
for series in heatmap.get("series", [])
for point in series.get("data", [])
if point.get("y") is not None
]
if not values:
return data
average = round(sum(values) / len(values))
data["stats"] = f"{average}%"
if average >= 60:
data["chipText"] = "بهینه"
data["chipColor"] = "success"
elif average >= 45:
data["chipText"] = "متوسط"
data["chipColor"] = "warning"
else:
data["chipText"] = "کم"
data["chipColor"] = "error"
data["avatarColor"] = "warning"
return data
def get_sensor_radar_chart_data(farm=None):
return deepcopy(SENSOR_RADAR_CHART)
def get_sensor_comparison_chart_data(farm=None):
return deepcopy(SENSOR_COMPARISON_CHART)
def get_anomaly_detection_card_data(farm=None):
if farm is None:
return deepcopy(ANOMALY_DETECTION_CARD)
anomalies = list(AnomalyDetection.objects.filter(farm=farm)[:10])
if not anomalies:
return deepcopy(ANOMALY_DETECTION_CARD)
return {
"anomalies": [
{
"sensor": anomaly.sensor,
"value": anomaly.value,
"expected": anomaly.expected,
"deviation": anomaly.deviation,
"severity": anomaly.severity,
}
for anomaly in anomalies
]
}
def get_soil_moisture_heatmap_data(farm=None):
return deepcopy(SOIL_MOISTURE_HEATMAP)
def get_soil_summary_data(farm=None):
return {
"avgSoilMoisture": get_avg_soil_moisture_data(farm),
"sensorRadarChart": get_sensor_radar_chart_data(farm),
"sensorComparisonChart": get_sensor_comparison_chart_data(farm),
"anomalyDetectionCard": get_anomaly_detection_card_data(farm),
"soilMoistureHeatmap": get_soil_moisture_heatmap_data(farm),
}
+19
View File
@@ -0,0 +1,19 @@
from django.urls import path
from .views import (
AvgSoilMoistureView,
SensorComparisonChartView,
SensorRadarChartView,
SoilAnomalyDetectionView,
SoilMoistureHeatmapView,
SoilSummaryView,
)
urlpatterns = [
path("avg-moisture/", AvgSoilMoistureView.as_view(), name="soil-avg-moisture"),
path("sensor-radar-chart/", SensorRadarChartView.as_view(), name="soil-sensor-radar-chart"),
path("sensor-comparison-chart/", SensorComparisonChartView.as_view(), name="soil-sensor-comparison-chart"),
path("anomalies/", SoilAnomalyDetectionView.as_view(), name="soil-anomalies"),
path("moisture-heatmap/", SoilMoistureHeatmapView.as_view(), name="soil-moisture-heatmap"),
path("summary/", SoilSummaryView.as_view(), name="soil-summary"),
]
+132
View File
@@ -0,0 +1,132 @@
from rest_framework import status
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 status_response
from farm_hub.models import FarmHub
from .serializers import (
SoilAnomalyDetectionSerializer,
SoilComparisonChartSerializer,
SoilKpiSerializer,
SoilMoistureHeatmapSerializer,
SoilRadarChartSerializer,
SoilSummarySerializer,
)
from .services import (
get_anomaly_detection_card_data,
get_avg_soil_moisture_data,
get_sensor_comparison_chart_data,
get_sensor_radar_chart_data,
get_soil_moisture_heatmap_data,
get_soil_summary_data,
)
def _get_farm_from_request(request):
farm_uuid = request.query_params.get("farm_uuid")
if not farm_uuid:
return None
try:
return FarmHub.objects.get(farm_uuid=farm_uuid)
except (FarmHub.DoesNotExist, Exception):
return None
class AvgSoilMoistureView(APIView):
@extend_schema(
tags=["Soil"],
parameters=[
OpenApiParameter(
name="farm_uuid",
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
required=False,
description="UUID of the farm for average soil moisture.",
default="11111111-1111-1111-1111-111111111111",
),
],
responses={200: status_response("AvgSoilMoistureResponse", data=SoilKpiSerializer())},
)
def get(self, request):
return Response(
{"status": "success", "data": get_avg_soil_moisture_data(_get_farm_from_request(request))},
status=status.HTTP_200_OK,
)
class SensorRadarChartView(APIView):
@extend_schema(
tags=["Soil"],
parameters=[
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False),
],
responses={200: status_response("SensorRadarChartResponse", data=SoilRadarChartSerializer())},
)
def get(self, request):
return Response(
{"status": "success", "data": get_sensor_radar_chart_data(_get_farm_from_request(request))},
status=status.HTTP_200_OK,
)
class SensorComparisonChartView(APIView):
@extend_schema(
tags=["Soil"],
parameters=[
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False),
],
responses={200: status_response("SensorComparisonChartResponse", data=SoilComparisonChartSerializer())},
)
def get(self, request):
return Response(
{"status": "success", "data": get_sensor_comparison_chart_data(_get_farm_from_request(request))},
status=status.HTTP_200_OK,
)
class SoilAnomalyDetectionView(APIView):
@extend_schema(
tags=["Soil"],
parameters=[
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False),
],
responses={200: status_response("SoilAnomalyDetectionResponse", data=SoilAnomalyDetectionSerializer())},
)
def get(self, request):
return Response(
{"status": "success", "data": get_anomaly_detection_card_data(_get_farm_from_request(request))},
status=status.HTTP_200_OK,
)
class SoilMoistureHeatmapView(APIView):
@extend_schema(
tags=["Soil"],
parameters=[
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False),
],
responses={200: status_response("SoilMoistureHeatmapResponse", data=SoilMoistureHeatmapSerializer())},
)
def get(self, request):
return Response(
{"status": "success", "data": get_soil_moisture_heatmap_data(_get_farm_from_request(request))},
status=status.HTTP_200_OK,
)
class SoilSummaryView(APIView):
@extend_schema(
tags=["Soil"],
parameters=[
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False),
],
responses={200: status_response("SoilSummaryResponse", data=SoilSummarySerializer())},
)
def get(self, request):
return Response(
{"status": "success", "data": get_soil_summary_data(_get_farm_from_request(request))},
status=status.HTTP_200_OK,
)