UPDATE
This commit is contained in:
@@ -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"
|
||||
@@ -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"]},
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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},
|
||||
)
|
||||
@@ -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"),
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user