This commit is contained in:
2026-04-10 16:12:51 +03:30
parent 20fd3842b6
commit 883573004c
143 changed files with 1380 additions and 2332 deletions
View File
+7
View File
@@ -0,0 +1,7 @@
from django.apps import AppConfig
class FarmAlertsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "farm_alerts"
verbose_name = "Farm Alerts"
+61
View File
@@ -0,0 +1,61 @@
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("farm_hub", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="FarmAlert",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True, db_index=True)),
("farm", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="farm_alerts", to="farm_hub.farmhub")),
("title", models.CharField(max_length=255)),
("description", models.TextField(blank=True, default="")),
("color", models.CharField(default="info", max_length=32)),
("avatar_icon", models.CharField(blank=True, default="", max_length=64)),
("avatar_color", models.CharField(blank=True, default="", max_length=32)),
("is_active", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={"db_table": "farm_alerts", "ordering": ["-created_at"]},
),
migrations.CreateModel(
name="AnomalyDetection",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True, db_index=True)),
("farm", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="anomalies", to="farm_hub.farmhub")),
("sensor", models.CharField(max_length=255)),
("value", models.CharField(max_length=64)),
("expected", models.CharField(max_length=64)),
("deviation", models.CharField(max_length=64)),
("severity", models.CharField(default="warning", max_length=32)),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={"db_table": "farm_anomaly_detections", "ordering": ["-created_at"]},
),
migrations.CreateModel(
name="Recommendation",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True, db_index=True)),
("farm", models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="recommendations", to="farm_hub.farmhub")),
("title", models.CharField(max_length=255)),
("subtitle", models.TextField(blank=True, default="")),
("avatar_icon", models.CharField(blank=True, default="", max_length=64)),
("avatar_color", models.CharField(blank=True, default="", max_length=32)),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={"db_table": "farm_recommendations", "ordering": ["-created_at"]},
),
]
View File
+101
View File
@@ -0,0 +1,101 @@
ARM_ALERTS_TRACKER = {
"totalAlerts": 3,
"radialBarValue": 30,
"alertStats": [
{
"title": "کمبود آب",
"count": "2",
"avatarColor": "error",
"avatarIcon": "tabler-droplet-half-2",
},
{
"title": "ریسک قارچی",
"count": "1",
"avatarColor": "warning",
"avatarIcon": "tabler-mushroom",
},
{
"title": "هشدار یخبندان",
"count": "0",
"avatarColor": "info",
"avatarIcon": "tabler-snowflake",
},
],
}
FARM_ALERTS_TIMELINE = {
"alerts": [
{
"title": "ریسک کمبود آب",
"description": "رطوبت خاک در عمق ۱۰ سانتی‌متر (۴۲٪) کمتر از حد بهینه است. پیش‌بینی: در صورت عدم آبیاری، تنش طی ۲ تا ۳ روز. توصیه: آبیاری ظرف ۲۴ ساعت.",
"time": "۱۵ دقیقه پیش",
"color": "warning",
},
{
"title": "ریسک بیماری قارچی",
"description": "رطوبت بالا (۶۵٪) و دمای ۲۴ درجه شرایط مساعد برای رشد قارچ. استفاده از قارچ‌کش پیشگیرانه یا کاهش آبیاری را در نظر بگیرید.",
"time": "۱ ساعت پیش",
"color": "error",
},
{
"title": "پیشنهاد آبیاری",
"description": "بازه بهینه آبیاری: ۶:۰۰ تا ۸:۰۰ صبح. حجم پیشنهادی: ۴۵۰ مترمکعب برای زون آ. بهبود راندمان مورد انتظار: ۱۲٪.",
"time": "۲ ساعت پیش",
"color": "info",
},
{
"title": "بررسی شوری خاک",
"description": "مقدار هدایت الکتریکی ۱/۲ dS/m در محدوده مجاز است. نیازی به اقدام نیست. بررسی بعدی توصیه می‌شود ظرف ۵ روز.",
"time": "۴ ساعت پیش",
"color": "success",
},
]
}
ANOMALY_DETECTION_CARD = {
"anomalies": [
{
"sensor": "رطوبت خاک زون ۳",
"value": "38%",
"expected": "45-65%",
"deviation": "-12%",
"severity": "warning",
},
{
"sensor": "pH بخش ۲",
"value": "5.2",
"expected": "6.0-7.0",
"deviation": "-0.8",
"severity": "error",
},
]
}
RECOMMENDATIONS_LIST = {
"recommendations": [
{
"title": "آبیاری: ۶:۰۰ تا ۸:۰۰ صبح",
"subtitle": "۴۵۰ مترمکعب برای زون آ. بدون آبیاری، عملکرد ممکن است حدود ۸٪ کاهش یابد.",
"avatarIcon": "tabler-droplet",
"avatarColor": "primary",
},
{
"title": "کود: NPK 20-20-20",
"subtitle": "اعمال ۲۵ کیلوگرم در هکتار ظرف ۷ روز. کمبود نیتروژن فعلی در بخش ۲.",
"avatarIcon": "tabler-leaf",
"avatarColor": "success",
},
{
"title": "قارچ‌کش: پیشگیرانه",
"subtitle": "رطوبت و دما مساعد قارچ. سمپاشی بر پایه مس را در نظر بگیرید.",
"avatarIcon": "tabler-mushroom",
"avatarColor": "warning",
},
{
"title": "بازه برداشت: ۱۲ تا ۱۸ اکتبر",
"subtitle": "اوج رسیدگی حدود ۱۵ اکتبر. نیروی کار را متناسب برنامه‌ریزی کنید.",
"avatarIcon": "tabler-calendar-event",
"avatarColor": "info",
},
]
}
+67
View File
@@ -0,0 +1,67 @@
import uuid as uuid_lib
from django.db import models
from farm_hub.models import FarmHub
SEVERITY_CHOICES = [
("info", "Info"),
("warning", "Warning"),
("error", "Error"),
("success", "Success"),
]
class FarmAlert(models.Model):
uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
farm = models.ForeignKey(FarmHub, on_delete=models.CASCADE, related_name="farm_alerts", null=True, blank=True)
title = models.CharField(max_length=255)
description = models.TextField(blank=True, default="")
color = models.CharField(max_length=32, default="info", choices=SEVERITY_CHOICES)
avatar_icon = models.CharField(max_length=64, blank=True, default="")
avatar_color = models.CharField(max_length=32, blank=True, default="")
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "farm_alerts"
ordering = ["-created_at"]
def __str__(self):
return f"{self.title} ({self.color})"
class AnomalyDetection(models.Model):
uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
farm = models.ForeignKey(FarmHub, on_delete=models.CASCADE, related_name="anomalies", null=True, blank=True)
sensor = models.CharField(max_length=255)
value = models.CharField(max_length=64)
expected = models.CharField(max_length=64)
deviation = models.CharField(max_length=64)
severity = models.CharField(max_length=32, default="warning", choices=SEVERITY_CHOICES)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "farm_anomaly_detections"
ordering = ["-created_at"]
def __str__(self):
return f"{self.sensor}: {self.value}"
class Recommendation(models.Model):
uuid = models.UUIDField(default=uuid_lib.uuid4, unique=True, editable=False, db_index=True)
farm = models.ForeignKey(FarmHub, on_delete=models.CASCADE, related_name="recommendations", null=True, blank=True)
title = models.CharField(max_length=255)
subtitle = models.TextField(blank=True, default="")
avatar_icon = models.CharField(max_length=64, blank=True, default="")
avatar_color = models.CharField(max_length=32, blank=True, default="")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "farm_recommendations"
ordering = ["-created_at"]
def __str__(self):
return self.title
+57
View File
@@ -0,0 +1,57 @@
from rest_framework import serializers
class AlertStatSerializer(serializers.Serializer):
title = serializers.CharField()
count = serializers.CharField()
avatarColor = serializers.CharField()
avatarIcon = serializers.CharField()
class AlertTrackerSerializer(serializers.Serializer):
totalAlerts = serializers.IntegerField()
radialBarValue = serializers.IntegerField()
alertStats = AlertStatSerializer(many=True)
class AlertTimelineItemSerializer(serializers.Serializer):
title = serializers.CharField()
description = serializers.CharField()
time = serializers.CharField()
color = serializers.CharField()
class AlertTimelineSerializer(serializers.Serializer):
alerts = AlertTimelineItemSerializer(many=True)
class AnomalyItemSerializer(serializers.Serializer):
sensor = serializers.CharField()
value = serializers.CharField()
expected = serializers.CharField()
deviation = serializers.CharField()
severity = serializers.CharField()
class AnomalyDetectionSerializer(serializers.Serializer):
anomalies = AnomalyItemSerializer(many=True)
class RecommendationItemSerializer(serializers.Serializer):
title = serializers.CharField()
subtitle = serializers.CharField()
avatarIcon = serializers.CharField()
avatarColor = serializers.CharField()
class RecommendationsListSerializer(serializers.Serializer):
recommendations = RecommendationItemSerializer(many=True)
class CreateAlertSerializer(serializers.Serializer):
farm_uuid = serializers.UUIDField(required=False, allow_null=True)
title = serializers.CharField(max_length=255)
description = serializers.CharField(required=False, default="", allow_blank=True)
color = serializers.ChoiceField(choices=["info", "warning", "error", "success"], default="info")
avatar_icon = serializers.CharField(required=False, default="", allow_blank=True)
avatar_color = serializers.CharField(required=False, default="", allow_blank=True)
+49
View File
@@ -0,0 +1,49 @@
from farm_hub.models import FarmHub
from notifications.models import FarmNotification
from .models import FarmAlert
class AlertService:
@staticmethod
def create_alert(
title: str,
description: str = "",
color: str = "info",
avatar_icon: str = "",
avatar_color: str = "",
farm_uuid=None,
) -> FarmAlert:
farm = None
if farm_uuid:
try:
farm = FarmHub.objects.get(uuid=farm_uuid)
except FarmHub.DoesNotExist:
pass
alert = FarmAlert.objects.create(
farm=farm,
title=title,
description=description,
color=color,
avatar_icon=avatar_icon,
avatar_color=avatar_color,
)
AlertService._send_notification(alert, farm)
return alert
@staticmethod
def _send_notification(alert: FarmAlert, farm) -> None:
if farm is None:
return
level_map = {"error": "error", "warning": "warning", "info": "info", "success": "success"}
FarmNotification.objects.create(
farm=farm,
title=alert.title,
message=alert.description,
level=level_map.get(alert.color, "info"),
metadata={"alert_uuid": str(alert.uuid), "color": alert.color},
)
+17
View File
@@ -0,0 +1,17 @@
from django.urls import path
from .views import (
AlertTrackerView,
AlertTimelineView,
AnomalyDetectionView,
RecommendationsListView,
CreateAlertView,
)
urlpatterns = [
path("tracker/", AlertTrackerView.as_view(), name="farm-alerts-tracker"),
path("timeline/", AlertTimelineView.as_view(), name="farm-alerts-timeline"),
path("anomalies/", AnomalyDetectionView.as_view(), name="farm-alerts-anomalies"),
path("recommendations/", RecommendationsListView.as_view(), name="farm-alerts-recommendations"),
path("create/", CreateAlertView.as_view(), name="farm-alerts-create"),
]
+80
View File
@@ -0,0 +1,80 @@
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from farm_hub.models import FarmHub
from .mock_data import (
ANOMALY_DETECTION_CARD,
ARM_ALERTS_TRACKER,
FARM_ALERTS_TIMELINE,
RECOMMENDATIONS_LIST,
)
from .serializers import (
AlertTimelineSerializer,
AlertTrackerSerializer,
AnomalyDetectionSerializer,
CreateAlertSerializer,
RecommendationsListSerializer,
)
from .services import AlertService
class AlertTrackerView(APIView):
def get(self, request):
serializer = AlertTrackerSerializer(ARM_ALERTS_TRACKER)
return Response({"status": "success", "result": serializer.data})
class AlertTimelineView(APIView):
def get(self, request):
serializer = AlertTimelineSerializer(FARM_ALERTS_TIMELINE)
return Response({"status": "success", "result": serializer.data})
class AnomalyDetectionView(APIView):
def get(self, request):
serializer = AnomalyDetectionSerializer(ANOMALY_DETECTION_CARD)
return Response({"status": "success", "result": serializer.data})
class RecommendationsListView(APIView):
def get(self, request):
serializer = RecommendationsListSerializer(RECOMMENDATIONS_LIST)
return Response({"status": "success", "result": serializer.data})
class CreateAlertView(APIView):
def post(self, request):
serializer = CreateAlertSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"status": "error", "errors": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
data = serializer.validated_data
farm = None
farm_uuid = data.get("farm_uuid")
if farm_uuid:
try:
farm = FarmHub.objects.get(uuid=farm_uuid)
except FarmHub.DoesNotExist:
return Response(
{"status": "error", "message": "farm not found"},
status=status.HTTP_404_NOT_FOUND,
)
alert = AlertService.create_alert(
title=data["title"],
description=data.get("description", ""),
color=data.get("color", "info"),
avatar_icon=data.get("avatar_icon", ""),
avatar_color=data.get("avatar_color", ""),
farm=farm,
)
return Response(
{"status": "success", "result": {"uuid": str(alert.uuid), "title": alert.title}},
status=status.HTTP_201_CREATED,
)