UPDATE
This commit is contained in:
@@ -78,3 +78,65 @@ pH
|
||||
6.8
|
||||
بهترین جذب مواد مغذی را دارد.
|
||||
فاصله کوددهی کلسیم و فسفر: کودهای حاوی کلسیم را هرگز با کودهای حاوی فسفر یا سولفات همزمان مخلوط نکنید (رسوب میکنند).
|
||||
|
||||
راهنمای کوددهی هویج
|
||||
قاعده کلی برای هویج: هویج به نیتروژن (
|
||||
𝑁
|
||||
N
|
||||
) کم تا متوسط، اما به فسفر (
|
||||
𝑃
|
||||
P
|
||||
) و به ویژه پتاسیم (
|
||||
𝐾
|
||||
K
|
||||
) بسیار بالایی نیاز دارد.
|
||||
مراحل کوددهی:
|
||||
آمادهسازی خاک (قبل از کاشت): استفاده از کودهای پایه فسفر و پتاسیم. هشدار مهم: به هیچ وجه از کود دامی تازه استفاده نکنید! کود دامی باید کاملاً پوسیده باشد. کود حیوانی تازه باعث دو یا چند شاخه شدن هویج و ایجاد ریشههای مویی زائد میشود.
|
||||
رشد رویشی (اوایل رشد): استفاده محدود از نیتروژن برای رشد برگها. نیتروژن بیش از حد باعث میشود گیاه تمام انرژی خود را صرف تولید برگ کند و ریشه (بخش خوراکی) نازک و کوچک بماند.
|
||||
رشد و حجم گرفتن ریشه (اواسط تا اواخر رشد): استفاده از کودهای پتاسبالا (مانند سولوپتاس یا کودهای
|
||||
12
|
||||
−
|
||||
12
|
||||
−
|
||||
36
|
||||
12−12−36
|
||||
) برای افزایش سایز، بهبود رنگ، طعم شیرینتر و تردی هویج.
|
||||
عناصر ریزمغذی کلیدی:
|
||||
بُر (
|
||||
𝐵
|
||||
B
|
||||
): یکی از مهمترین عناصر برای هویج است. کمبود بُر باعث ایجاد شکاف در ریشه، سیاه شدن مغز هویج و کاهش بازارپسندی میشود.
|
||||
کلسیم (
|
||||
𝐶
|
||||
𝑎
|
||||
Ca
|
||||
): برای استحکام بافت ریشه و جلوگیری از بیماریها در انبار مهم است.
|
||||
خلاصه نکات طلایی و مشکلات رایج
|
||||
دو یا چند شاخه شدن هویج (Forking): ناشی از استفاده از کود دامی تازه، وجود سنگ و کلوخ در خاک، یا خاکهای بسیار سفت و رسی است. خاک هویج باید تا عمق حداقل
|
||||
25
|
||||
25
|
||||
سانتیمتری پوک و سبک باشد.
|
||||
ترکخوردگی ریشه: ناشی از نوسانات آبیاری (خاک خشک شود و ناگهان غرقاب گردد) یا دریافت بیش از حد نیتروژن در اواخر رشد.
|
||||
ریشههای مویی فراوان روی هویج: ناشی از مصرف بیش از حد کودهای نیتروژنه (
|
||||
𝑁
|
||||
N
|
||||
) یا رطوبت دائمی و بیش از حد خاک است.
|
||||
تنظیم
|
||||
𝑝
|
||||
𝐻
|
||||
pH
|
||||
خاک: هویج در خاکهایی با
|
||||
𝑝
|
||||
𝐻
|
||||
pH
|
||||
بین
|
||||
6.0
|
||||
6.0
|
||||
تا
|
||||
6.8
|
||||
6.8
|
||||
بهترین رشد را دارد. در
|
||||
𝑝
|
||||
𝐻
|
||||
pH
|
||||
پایینتر (خاک اسیدی)، رشد ریشه متوقف میشود.
|
||||
@@ -13,3 +13,14 @@
|
||||
زمان آبیاری: بهترین زمان، صبح زود است تا گیاه در طول روز رطوبت کافی داشته باشد و برگها تا شب خشک شوند.
|
||||
عمق آبیاری: آبیاری باید عمیق باشد تا ریشهها به عمق خاک نفوذ کنند (حداقل ۱۵ تا ۲۰ سانتیمتر).
|
||||
مالچپاشی: استفاده از مالچ (پلاستیک کشاورزی یا کاه و کلش) روی خاک، رطوبت را حفظ کرده و از تبخیر سریع آب جلوگیری میکند.
|
||||
|
||||
راهنمای آبیاری هویج
|
||||
اهمیت رطوبت در هویج: هویج یک گیاه ریشهای است و کیفیت ریشه آن ارتباط مستقیمی با نحوه آبیاری دارد. نوسانات رطوبتی باعث افت شدید کیفیت محصول میشود.
|
||||
نیاز آبی در مراحل مختلف رشد:
|
||||
کاشت و جوانهزنی: بذر هویج بسیار ریز است و در عمق کم کاشته میشود. در این مرحله خاک باید دائماً مرطوب (اما نه غرقاب) باشد تا بذرها خشک نشوند. خشکی در این مرحله باعث عدم سبز شدن بذرها میشود.
|
||||
رشد اولیه و توسعه ریشه: پس از سبز شدن، آبیاری باید عمیقتر و با فواصل بیشتر انجام شود تا ریشه گیاه برای پیدا کردن آب به عمق خاک نفوذ کند. آبیاری سطحی باعث کوتاه ماندن هویج میشود.
|
||||
حجم گرفتن ریشه (غدهبندی): نیاز آبی در این مرحله بالاست. رطوبت باید یکنواخت باشد.
|
||||
نزدیک به برداشت: کاهش آبیاری در اواخر دوره رشد ضروری است. آبیاری زیاد در این مرحله باعث ترکخوردگی هویجها میشود.
|
||||
روشهای آبیاری:
|
||||
بهترین روش: آبیاری قطرهای (نوار تیپ) زیرا رطوبت را به صورت یکنواخت در اختیار ریشه قرار میدهد و از بیماریهای برگی جلوگیری میکند.
|
||||
تنش آبی: خشک و خیس شدن پیاپی خاک، عامل اصلی دو شاخه شدن و ترک خوردن هویج است.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("irrigation", "0001_initial"),
|
||||
("sensor_data", "0010_rename_tables_to_farm_data"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="sensordata",
|
||||
name="irrigation_method",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_column="irrigation_method_id",
|
||||
help_text="روش آبیاری انتخابشده برای این farm",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="farm_data",
|
||||
to="irrigation.irrigationmethod",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -115,6 +115,15 @@ class SensorData(SensorPayloadMixin, models.Model):
|
||||
related_name="farm_data",
|
||||
help_text="گیاهان مرتبط با این farm",
|
||||
)
|
||||
irrigation_method = models.ForeignKey(
|
||||
"irrigation.IrrigationMethod",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="farm_data",
|
||||
db_column="irrigation_method_id",
|
||||
help_text="روش آبیاری انتخابشده برای این farm",
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from location_data.serializers import SoilDepthDataSerializer
|
||||
from irrigation.models import IrrigationMethod
|
||||
from irrigation.serializers import IrrigationMethodSerializer
|
||||
from plant.serializers import PlantSerializer
|
||||
from weather.models import WeatherForecast
|
||||
|
||||
@@ -19,6 +21,11 @@ class SensorDataUpdateSerializer(serializers.Serializer):
|
||||
required=False,
|
||||
help_text="لیست شناسه گیاهان مرتبط",
|
||||
)
|
||||
irrigation_method_id = serializers.IntegerField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text="شناسه روش آبیاری مرتبط",
|
||||
)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if not isinstance(data, dict):
|
||||
@@ -31,6 +38,7 @@ class SensorDataUpdateSerializer(serializers.Serializer):
|
||||
"sensor_key",
|
||||
"sensor_payload",
|
||||
"plant_ids",
|
||||
"irrigation_method_id",
|
||||
}
|
||||
flat_metrics = {
|
||||
key: value
|
||||
@@ -71,10 +79,21 @@ class SensorDataUpdateSerializer(serializers.Serializer):
|
||||
)
|
||||
return value
|
||||
|
||||
def validate_irrigation_method_id(self, value):
|
||||
if value is None:
|
||||
return value
|
||||
if not IrrigationMethod.objects.filter(pk=value).exists():
|
||||
raise serializers.ValidationError("روش آبیاری معتبر نیست.")
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
if "sensor_payload" not in attrs and "plant_ids" not in attrs:
|
||||
if (
|
||||
"sensor_payload" not in attrs
|
||||
and "plant_ids" not in attrs
|
||||
and "irrigation_method_id" not in attrs
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"حداقل یکی از sensor_payload یا plant_ids باید ارسال شود."
|
||||
"حداقل یکی از sensor_payload یا plant_ids یا irrigation_method_id باید ارسال شود."
|
||||
)
|
||||
return attrs
|
||||
|
||||
@@ -87,6 +106,11 @@ class SensorDataResponseSerializer(serializers.ModelSerializer):
|
||||
many=True,
|
||||
read_only=True,
|
||||
)
|
||||
irrigation_method_id = serializers.IntegerField(
|
||||
source="irrigation_method.id",
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SensorData
|
||||
@@ -96,6 +120,7 @@ class SensorDataResponseSerializer(serializers.ModelSerializer):
|
||||
"weather_forecast_id",
|
||||
"sensor_payload",
|
||||
"plant_ids",
|
||||
"irrigation_method_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
@@ -148,12 +173,13 @@ class FarmSoilPayloadSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class FarmDetailSerializer(serializers.Serializer):
|
||||
farm_uuid = serializers.UUIDField()
|
||||
center_location = FarmCenterLocationSerializer()
|
||||
weather = WeatherForecastDetailSerializer(allow_null=True)
|
||||
sensor_payload = serializers.JSONField()
|
||||
soil = FarmSoilPayloadSerializer()
|
||||
plant_ids = serializers.ListField(child=serializers.IntegerField())
|
||||
plants = PlantSerializer(many=True)
|
||||
irrigation_method_id = serializers.IntegerField(allow_null=True)
|
||||
irrigation_method = IrrigationMethodSerializer(allow_null=True)
|
||||
created_at = serializers.DateTimeField()
|
||||
updated_at = serializers.DateTimeField()
|
||||
|
||||
+12
-2
@@ -7,6 +7,7 @@ from django.db import transaction
|
||||
from location_data.models import SoilLocation
|
||||
from location_data.serializers import SoilDepthDataSerializer
|
||||
from location_data.tasks import fetch_soil_data_for_coordinates
|
||||
from irrigation.serializers import IrrigationMethodSerializer
|
||||
from plant.serializers import PlantSerializer
|
||||
from weather.services import update_weather_for_location
|
||||
from weather.models import WeatherForecast
|
||||
@@ -25,7 +26,11 @@ class ExternalDataSyncError(Exception):
|
||||
|
||||
def get_farm_details(farm_uuid: str):
|
||||
farm = (
|
||||
SensorData.objects.select_related("center_location", "weather_forecast")
|
||||
SensorData.objects.select_related(
|
||||
"center_location",
|
||||
"weather_forecast",
|
||||
"irrigation_method",
|
||||
)
|
||||
.prefetch_related("plants", "center_location__depths")
|
||||
.filter(farm_uuid=farm_uuid)
|
||||
.first()
|
||||
@@ -53,7 +58,6 @@ def get_farm_details(farm_uuid: str):
|
||||
metric_sources[key] = "sensor"
|
||||
|
||||
return {
|
||||
"farm_uuid": farm.farm_uuid,
|
||||
"center_location": {
|
||||
"id": center_location.id,
|
||||
"lat": center_location.latitude,
|
||||
@@ -69,6 +73,12 @@ def get_farm_details(farm_uuid: str):
|
||||
},
|
||||
"plant_ids": list(farm.plants.values_list("id", flat=True)),
|
||||
"plants": PlantSerializer(farm.plants.all(), many=True).data,
|
||||
"irrigation_method_id": farm.irrigation_method_id,
|
||||
"irrigation_method": (
|
||||
IrrigationMethodSerializer(farm.irrigation_method).data
|
||||
if farm.irrigation_method
|
||||
else None
|
||||
),
|
||||
"created_at": farm.created_at,
|
||||
"updated_at": farm.updated_at,
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ from rest_framework.test import APIClient
|
||||
|
||||
from location_data.models import SoilDepthData, SoilLocation
|
||||
from farm_data.models import SensorData
|
||||
from irrigation.models import IrrigationMethod
|
||||
from plant.models import Plant
|
||||
from weather.models import WeatherForecast
|
||||
|
||||
@@ -58,11 +59,13 @@ class FarmDetailApiTests(TestCase):
|
||||
)
|
||||
self.plant1 = Plant.objects.create(name="گوجهفرنگی")
|
||||
self.plant2 = Plant.objects.create(name="خیار")
|
||||
self.irrigation_method = IrrigationMethod.objects.create(name="آبیاری قطرهای")
|
||||
self.farm_uuid = uuid.uuid4()
|
||||
self.farm = SensorData.objects.create(
|
||||
farm_uuid=self.farm_uuid,
|
||||
center_location=self.location,
|
||||
weather_forecast=self.weather,
|
||||
irrigation_method=self.irrigation_method,
|
||||
sensor_payload={
|
||||
"sensor-7-1": {
|
||||
"soil_moisture": 33.5,
|
||||
@@ -78,7 +81,7 @@ class FarmDetailApiTests(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
payload = response.json()["data"]
|
||||
|
||||
self.assertEqual(payload["farm_uuid"], str(self.farm_uuid))
|
||||
self.assertNotIn("farm_uuid", payload)
|
||||
self.assertEqual(payload["center_location"]["id"], self.location.id)
|
||||
self.assertEqual(payload["weather"]["id"], self.weather.id)
|
||||
self.assertEqual(
|
||||
@@ -100,6 +103,8 @@ class FarmDetailApiTests(TestCase):
|
||||
self.assertEqual(returned_plants[self.plant1.id]["name"], self.plant1.name)
|
||||
self.assertEqual(returned_plants[self.plant2.id]["name"], self.plant2.name)
|
||||
self.assertIn("light", returned_plants[self.plant1.id])
|
||||
self.assertEqual(payload["irrigation_method_id"], self.irrigation_method.id)
|
||||
self.assertEqual(payload["irrigation_method"]["name"], self.irrigation_method.name)
|
||||
|
||||
def test_returns_404_when_farm_is_missing(self):
|
||||
response = self.client.get(f"/api/farm-data/{uuid.uuid4()}/detail/")
|
||||
@@ -123,6 +128,7 @@ class FarmDataUpsertApiTests(TestCase):
|
||||
temperature_max=24.0,
|
||||
temperature_mean=17.5,
|
||||
)
|
||||
self.irrigation_method = IrrigationMethod.objects.create(name="بارانی")
|
||||
|
||||
def test_post_creates_farm_data_with_explicit_farm_uuid(self):
|
||||
farm_uuid = uuid.uuid4()
|
||||
@@ -138,6 +144,7 @@ class FarmDataUpsertApiTests(TestCase):
|
||||
"nitrogen": 18.0,
|
||||
}
|
||||
},
|
||||
"irrigation_method_id": self.irrigation_method.id,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -150,6 +157,7 @@ class FarmDataUpsertApiTests(TestCase):
|
||||
farm = SensorData.objects.get(farm_uuid=farm_uuid)
|
||||
self.assertEqual(farm.center_location_id, self.location.id)
|
||||
self.assertEqual(farm.weather_forecast_id, self.weather.id)
|
||||
self.assertEqual(farm.irrigation_method_id, self.irrigation_method.id)
|
||||
self.assertEqual(
|
||||
farm.sensor_payload["sensor-7-1"]["soil_moisture"],
|
||||
31.2,
|
||||
|
||||
@@ -168,6 +168,7 @@ class FarmDataUpsertView(APIView):
|
||||
farm_uuid = serializer.validated_data["farm_uuid"]
|
||||
farm_boundary = serializer.validated_data["farm_boundary"]
|
||||
plant_ids = serializer.validated_data.get("plant_ids")
|
||||
irrigation_method_id = serializer.validated_data.get("irrigation_method_id")
|
||||
sensor_payload = serializer.validated_data.get("sensor_payload", {})
|
||||
try:
|
||||
center_location = resolve_center_location_from_boundary(farm_boundary)
|
||||
@@ -210,12 +211,15 @@ class FarmDataUpsertView(APIView):
|
||||
|
||||
farm_data.center_location = center_location
|
||||
farm_data.weather_forecast = weather_forecast
|
||||
if "irrigation_method_id" in serializer.validated_data:
|
||||
farm_data.irrigation_method_id = irrigation_method_id
|
||||
if not created:
|
||||
farm_data.save(
|
||||
update_fields=[
|
||||
"center_location",
|
||||
"weather_forecast",
|
||||
"sensor_payload",
|
||||
"irrigation_method",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import FertilizationRecommendView, FertilizationRecommendStatusView
|
||||
from .views import FertilizationRecommendView
|
||||
|
||||
urlpatterns = [
|
||||
path("recommend/", FertilizationRecommendView.as_view(), name="fertilization-recommend"),
|
||||
path("recommend/<str:task_id>/status/", FertilizationRecommendStatusView.as_view(), name="fertilization-recommend-status"),
|
||||
]
|
||||
|
||||
+20
-55
@@ -10,31 +10,25 @@ from rest_framework.views import APIView
|
||||
from config.openapi import (
|
||||
build_envelope_serializer,
|
||||
build_response,
|
||||
build_task_queue_data_serializer,
|
||||
build_task_status_data_serializer,
|
||||
)
|
||||
|
||||
from .serializers import FertilizationRecommendRequestSerializer
|
||||
|
||||
|
||||
FertilizationQueueResponseSerializer = build_envelope_serializer(
|
||||
"FertilizationQueueResponseSerializer",
|
||||
build_task_queue_data_serializer("FertilizationQueueDataSerializer"),
|
||||
)
|
||||
FertilizationValidationErrorSerializer = build_envelope_serializer(
|
||||
"FertilizationValidationErrorSerializer",
|
||||
data_required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
FertilizationStatusResponseSerializer = build_envelope_serializer(
|
||||
"FertilizationStatusResponseSerializer",
|
||||
build_task_status_data_serializer("FertilizationStatusDataSerializer"),
|
||||
FertilizationResponseSerializer = build_envelope_serializer(
|
||||
"FertilizationResponseSerializer",
|
||||
data_schema=None,
|
||||
)
|
||||
|
||||
|
||||
class FertilizationRecommendView(APIView):
|
||||
"""
|
||||
توصیه کودهی با Celery.
|
||||
توصیه کودهی به صورت مستقیم.
|
||||
POST با sensor_uuid، plant_name، growth_stage.
|
||||
اطلاعات گیاه از plant app دریافت میشود.
|
||||
نیازی به دریافت نوع آبیاری نیست.
|
||||
@@ -44,21 +38,25 @@ class FertilizationRecommendView(APIView):
|
||||
tags=["Fertilization Recommendation"],
|
||||
summary="درخواست توصیه کودهی",
|
||||
description=(
|
||||
"دادههای سنسور و گیاه را دریافت کرده و یک تسک Celery "
|
||||
"برای تولید توصیه کودهی در صف قرار میدهد. "
|
||||
"دادههای سنسور و گیاه را دریافت کرده و "
|
||||
"توصیه کودهی را مستقیم برمیگرداند. "
|
||||
"اطلاعات گیاه از جدول Plant بارگذاری میشود. "
|
||||
"محاسبات مربوط به نیاز آبی در این endpoint انجام نمیشود و مستقل از توصیه کودهی است."
|
||||
),
|
||||
request=FertilizationRecommendRequestSerializer,
|
||||
responses={
|
||||
202: build_response(
|
||||
FertilizationQueueResponseSerializer,
|
||||
"تسک توصیه کودهی در صف قرار گرفت.",
|
||||
200: build_response(
|
||||
FertilizationResponseSerializer,
|
||||
"توصیه کودهی با موفقیت تولید شد.",
|
||||
),
|
||||
400: build_response(
|
||||
FertilizationValidationErrorSerializer,
|
||||
"پارامتر ورودی نامعتبر است.",
|
||||
),
|
||||
500: build_response(
|
||||
FertilizationValidationErrorSerializer,
|
||||
"خطا در تولید توصیه کودهی.",
|
||||
),
|
||||
},
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
@@ -73,7 +71,7 @@ class FertilizationRecommendView(APIView):
|
||||
],
|
||||
)
|
||||
def post(self, request):
|
||||
from rag.tasks import fertilization_recommendation_task
|
||||
from rag.services.fertilization import get_fertilization_recommendation
|
||||
|
||||
serializer = FertilizationRecommendRequestSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
@@ -88,53 +86,20 @@ class FertilizationRecommendView(APIView):
|
||||
growth_stage = validated.get("growth_stage")
|
||||
query = validated.get("query")
|
||||
|
||||
task = fertilization_recommendation_task.delay(
|
||||
try:
|
||||
result = get_fertilization_recommendation(
|
||||
sensor_uuid=sensor_uuid,
|
||||
plant_name=plant_name,
|
||||
growth_stage=growth_stage,
|
||||
query=query,
|
||||
)
|
||||
except Exception as exc:
|
||||
return Response(
|
||||
{
|
||||
"code": 202,
|
||||
"msg": "تسک توصیه کودهی در صف قرار گرفت.",
|
||||
"data": {
|
||||
"task_id": task.id,
|
||||
"status_url": f"/api/fertilization/recommend/{task.id}/status/",
|
||||
},
|
||||
},
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
{"code": 500, "msg": f"خطا در تولید توصیه کودهی: {exc}", "data": None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class FertilizationRecommendStatusView(APIView):
|
||||
"""وضعیت تسک توصیه کودهی."""
|
||||
|
||||
@extend_schema(
|
||||
tags=["Fertilization Recommendation"],
|
||||
summary="وضعیت تسک توصیه کودهی",
|
||||
description="وضعیت تسک Celery توصیه کودهی را برمیگرداند.",
|
||||
responses={
|
||||
200: build_response(
|
||||
FertilizationStatusResponseSerializer,
|
||||
"وضعیت فعلی تسک توصیه کودهی.",
|
||||
),
|
||||
},
|
||||
)
|
||||
def get(self, request, task_id):
|
||||
from celery.result import AsyncResult
|
||||
|
||||
result = AsyncResult(task_id)
|
||||
data = {"task_id": task_id, "status": result.state}
|
||||
if result.state == "PENDING":
|
||||
data["message"] = "تسک در صف یا یافت نشد."
|
||||
elif result.state == "PROGRESS":
|
||||
data["progress"] = result.info
|
||||
elif result.state == "SUCCESS":
|
||||
data["result"] = result.result
|
||||
elif result.state == "FAILURE":
|
||||
data["error"] = str(result.result)
|
||||
return Response(
|
||||
{"code": 200, "msg": "success", "data": data},
|
||||
{"code": 200, "msg": "success", "data": result},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@@ -4,12 +4,10 @@ from .views import (
|
||||
IrrigationMethodDetailView,
|
||||
IrrigationMethodListCreateView,
|
||||
IrrigationRecommendView,
|
||||
IrrigationRecommendStatusView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("", IrrigationMethodListCreateView.as_view(), name="irrigation-list-create"),
|
||||
path("<int:pk>/", IrrigationMethodDetailView.as_view(), name="irrigation-detail"),
|
||||
path("recommend/", IrrigationRecommendView.as_view(), name="irrigation-recommend"),
|
||||
path("recommend/<str:task_id>/status/", IrrigationRecommendStatusView.as_view(), name="irrigation-recommend-status"),
|
||||
]
|
||||
|
||||
+20
-55
@@ -6,8 +6,6 @@ from rest_framework.views import APIView
|
||||
from config.openapi import (
|
||||
build_envelope_serializer,
|
||||
build_response,
|
||||
build_task_queue_data_serializer,
|
||||
build_task_status_data_serializer,
|
||||
)
|
||||
|
||||
from .models import IrrigationMethod
|
||||
@@ -31,13 +29,9 @@ IrrigationValidationErrorSerializer = build_envelope_serializer(
|
||||
data_required=False,
|
||||
allow_null=True,
|
||||
)
|
||||
IrrigationQueueResponseSerializer = build_envelope_serializer(
|
||||
"IrrigationQueueResponseSerializer",
|
||||
build_task_queue_data_serializer("IrrigationQueueDataSerializer"),
|
||||
)
|
||||
IrrigationStatusResponseSerializer = build_envelope_serializer(
|
||||
"IrrigationStatusResponseSerializer",
|
||||
build_task_status_data_serializer("IrrigationStatusDataSerializer"),
|
||||
IrrigationRecommendResponseSerializer = build_envelope_serializer(
|
||||
"IrrigationRecommendResponseSerializer",
|
||||
data_schema=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -112,7 +106,7 @@ class IrrigationMethodListCreateView(APIView):
|
||||
|
||||
class IrrigationRecommendView(APIView):
|
||||
"""
|
||||
توصیه آبیاری با Celery.
|
||||
توصیه آبیاری به صورت مستقیم.
|
||||
POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name.
|
||||
اطلاعات گیاه از plant app و روش آبیاری از irrigation app دریافت میشود.
|
||||
"""
|
||||
@@ -121,21 +115,25 @@ class IrrigationRecommendView(APIView):
|
||||
tags=["Irrigation Recommendation"],
|
||||
summary="درخواست توصیه آبیاری",
|
||||
description=(
|
||||
"دادههای سنسور، گیاه و روش آبیاری را دریافت کرده و یک تسک Celery "
|
||||
"برای تولید توصیه آبیاری در صف قرار میدهد. "
|
||||
"دادههای سنسور، گیاه و روش آبیاری را دریافت کرده و "
|
||||
"توصیه آبیاری را مستقیم برمیگرداند. "
|
||||
"اطلاعات گیاه از جدول Plant و روش آبیاری از جدول IrrigationMethod بارگذاری میشود. "
|
||||
"محاسبات ET₀ و ETc با مدل FAO-56 در بکاند انجام میشود و مدل زبانی فقط توضیح برنامه آبیاری را تولید میکند."
|
||||
),
|
||||
request=IrrigationRecommendRequestSerializer,
|
||||
responses={
|
||||
202: build_response(
|
||||
IrrigationQueueResponseSerializer,
|
||||
"تسک توصیه آبیاری در صف قرار گرفت.",
|
||||
200: build_response(
|
||||
IrrigationRecommendResponseSerializer,
|
||||
"توصیه آبیاری با موفقیت تولید شد.",
|
||||
),
|
||||
400: build_response(
|
||||
IrrigationValidationErrorSerializer,
|
||||
"پارامتر ورودی نامعتبر است.",
|
||||
),
|
||||
500: build_response(
|
||||
IrrigationValidationErrorSerializer,
|
||||
"خطا در تولید توصیه آبیاری.",
|
||||
),
|
||||
},
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
@@ -151,7 +149,7 @@ class IrrigationRecommendView(APIView):
|
||||
],
|
||||
)
|
||||
def post(self, request):
|
||||
from rag.tasks import irrigation_recommendation_task
|
||||
from rag.services.irrigation import get_irrigation_recommendation
|
||||
|
||||
serializer = IrrigationRecommendRequestSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
@@ -167,55 +165,22 @@ class IrrigationRecommendView(APIView):
|
||||
irrigation_method_name = validated.get("irrigation_method_name")
|
||||
query = validated.get("query")
|
||||
|
||||
task = irrigation_recommendation_task.delay(
|
||||
try:
|
||||
result = get_irrigation_recommendation(
|
||||
sensor_uuid=sensor_uuid,
|
||||
plant_name=plant_name,
|
||||
growth_stage=growth_stage,
|
||||
irrigation_method_name=irrigation_method_name,
|
||||
query=query,
|
||||
)
|
||||
except Exception as exc:
|
||||
return Response(
|
||||
{
|
||||
"code": 202,
|
||||
"msg": "تسک توصیه آبیاری در صف قرار گرفت.",
|
||||
"data": {
|
||||
"task_id": task.id,
|
||||
"status_url": f"/api/irrigation/recommend/{task.id}/status/",
|
||||
},
|
||||
},
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
{"code": 500, "msg": f"خطا در تولید توصیه آبیاری: {exc}", "data": None},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class IrrigationRecommendStatusView(APIView):
|
||||
"""وضعیت تسک توصیه آبیاری."""
|
||||
|
||||
@extend_schema(
|
||||
tags=["Irrigation Recommendation"],
|
||||
summary="وضعیت تسک توصیه آبیاری",
|
||||
description="وضعیت تسک Celery توصیه آبیاری را برمیگرداند.",
|
||||
responses={
|
||||
200: build_response(
|
||||
IrrigationStatusResponseSerializer,
|
||||
"وضعیت فعلی تسک توصیه آبیاری.",
|
||||
),
|
||||
},
|
||||
)
|
||||
def get(self, request, task_id):
|
||||
from celery.result import AsyncResult
|
||||
|
||||
result = AsyncResult(task_id)
|
||||
data = {"task_id": task_id, "status": result.state}
|
||||
if result.state == "PENDING":
|
||||
data["message"] = "تسک در صف یا یافت نشد."
|
||||
elif result.state == "PROGRESS":
|
||||
data["progress"] = result.info
|
||||
elif result.state == "SUCCESS":
|
||||
data["result"] = result.result
|
||||
elif result.state == "FAILURE":
|
||||
data["error"] = str(result.result)
|
||||
return Response(
|
||||
{"code": 200, "msg": "success", "data": data},
|
||||
{"code": 200, "msg": "success", "data": result},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("plant", "0004_plant_growth_profile"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="plant",
|
||||
name="growth_stage",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="مرحله رشد",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -32,6 +32,11 @@ class Plant(models.Model):
|
||||
blank=True,
|
||||
help_text="دمای مناسب",
|
||||
)
|
||||
growth_stage = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text="مرحله رشد",
|
||||
)
|
||||
planting_season = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
|
||||
@@ -15,6 +15,7 @@ class PlantSerializer(serializers.ModelSerializer):
|
||||
"watering",
|
||||
"soil",
|
||||
"temperature",
|
||||
"growth_stage",
|
||||
"planting_season",
|
||||
"harvest_time",
|
||||
"spacing",
|
||||
|
||||
@@ -81,6 +81,7 @@ class PlantListCreateView(APIView):
|
||||
"watering": "منظم، هفتهای ۲-۳ بار",
|
||||
"soil": "لومی، غنی از مواد آلی",
|
||||
"temperature": "۲۰-۳۰ درجه سانتیگراد",
|
||||
"growth_stage": "رشد رویشی",
|
||||
"planting_season": "بهار",
|
||||
"harvest_time": "۷۰-۹۰ روز پس از کاشت",
|
||||
"spacing": "۴۵-۶۰ سانتیمتر",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from farm_data.models import SensorData
|
||||
from rag.api_provider import get_chat_client
|
||||
from rag.chat import (
|
||||
_complete_audit_log,
|
||||
@@ -78,13 +79,24 @@ def get_fertilization_recommendation(
|
||||
|
||||
user_query = query or "توصیه کودهی برای مزرعه من چیست؟"
|
||||
|
||||
sensor = (
|
||||
SensorData.objects.prefetch_related("plants")
|
||||
.filter(farm_uuid=sensor_uuid)
|
||||
.first()
|
||||
)
|
||||
resolved_plant_name = plant_name
|
||||
if not resolved_plant_name and sensor is not None:
|
||||
plant = sensor.plants.first()
|
||||
if plant is not None:
|
||||
resolved_plant_name = plant.name
|
||||
|
||||
context = build_rag_context(
|
||||
user_query, sensor_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID,
|
||||
)
|
||||
|
||||
extra_parts: list[str] = []
|
||||
if plant_name and growth_stage:
|
||||
plant_text = build_plant_text(plant_name, growth_stage)
|
||||
if resolved_plant_name and growth_stage:
|
||||
plant_text = build_plant_text(resolved_plant_name, growth_stage)
|
||||
if plant_text:
|
||||
extra_parts.append("[اطلاعات گیاه]\n" + plant_text)
|
||||
if extra_parts:
|
||||
|
||||
@@ -83,12 +83,20 @@ def get_irrigation_recommendation(
|
||||
|
||||
user_query = query or "توصیه آبیاری برای مزرعه من چیست؟"
|
||||
|
||||
sensor = SensorData.objects.select_related("center_location").prefetch_related("plants").filter(farm_uuid=sensor_uuid).first()
|
||||
sensor = (
|
||||
SensorData.objects.select_related("center_location", "irrigation_method")
|
||||
.prefetch_related("plants")
|
||||
.filter(farm_uuid=sensor_uuid)
|
||||
.first()
|
||||
)
|
||||
plant = None
|
||||
resolved_plant_name = plant_name
|
||||
if sensor is not None and plant_name:
|
||||
plant = sensor.plants.filter(name=plant_name).first()
|
||||
elif sensor is not None:
|
||||
plant = sensor.plants.first()
|
||||
if plant is not None:
|
||||
resolved_plant_name = plant.name
|
||||
crop_profile = resolve_crop_profile(plant, growth_stage=growth_stage)
|
||||
active_kc = resolve_kc(crop_profile, growth_stage=growth_stage)
|
||||
forecasts = []
|
||||
@@ -99,10 +107,17 @@ def get_irrigation_recommendation(
|
||||
.order_by("forecast_date")[:7]
|
||||
)
|
||||
efficiency_percent = None
|
||||
resolved_irrigation_method_name = irrigation_method_name
|
||||
method = None
|
||||
if irrigation_method_name:
|
||||
from irrigation.models import IrrigationMethod
|
||||
|
||||
method = IrrigationMethod.objects.filter(name=irrigation_method_name).first()
|
||||
elif sensor is not None:
|
||||
method = sensor.irrigation_method
|
||||
if method is not None:
|
||||
resolved_irrigation_method_name = method.name
|
||||
|
||||
efficiency_percent = getattr(method, "water_efficiency_percent", None) if method else None
|
||||
daily_water_needs = calculate_forecast_water_needs(
|
||||
forecasts=forecasts,
|
||||
@@ -117,12 +132,15 @@ def get_irrigation_recommendation(
|
||||
)
|
||||
|
||||
extra_parts: list[str] = []
|
||||
if plant_name and growth_stage:
|
||||
plant_text = build_plant_text(plant_name, growth_stage)
|
||||
resolved_irrigation_method_name = irrigation_method_name or (
|
||||
sensor.irrigation_method.name if sensor is not None and sensor.irrigation_method else None
|
||||
)
|
||||
if resolved_plant_name and growth_stage:
|
||||
plant_text = build_plant_text(resolved_plant_name, growth_stage)
|
||||
if plant_text:
|
||||
extra_parts.append("[اطلاعات گیاه]\n" + plant_text)
|
||||
if irrigation_method_name:
|
||||
method_text = build_irrigation_method_text(irrigation_method_name)
|
||||
if resolved_irrigation_method_name:
|
||||
method_text = build_irrigation_method_text(resolved_irrigation_method_name)
|
||||
if method_text:
|
||||
extra_parts.append("[روش آبیاری انتخابی]\n" + method_text)
|
||||
if daily_water_needs:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from rag.chat import build_chat_context
|
||||
|
||||
|
||||
class ChatContextTests(SimpleTestCase):
|
||||
@patch("rag.chat.search_with_query")
|
||||
@patch("rag.chat._rank_text_chunks_by_query")
|
||||
@patch("rag.chat.chunk_text")
|
||||
def test_build_chat_context_combines_farm_and_kb_context(
|
||||
self,
|
||||
mock_chunk_text,
|
||||
mock_rank_text_chunks_by_query,
|
||||
mock_search_with_query,
|
||||
):
|
||||
mock_chunk_text.return_value = ["chunk-a", "chunk-b"]
|
||||
mock_rank_text_chunks_by_query.return_value = ["chunk-b"]
|
||||
mock_search_with_query.return_value = [
|
||||
{"text": "kb text 1"},
|
||||
{"text": "kb text 2"},
|
||||
]
|
||||
|
||||
context = build_chat_context(
|
||||
query="وضعیت مزرعه چطور است؟",
|
||||
farm_uuid="farm-123",
|
||||
farm_details={"sensor_payload": {"sensor-7-1": {"soil_moisture": 30}}},
|
||||
)
|
||||
|
||||
self.assertIn("[بخشهای مرتبط بازیابیشده از اطلاعات مزرعه]", context)
|
||||
self.assertIn("chunk-b", context)
|
||||
self.assertIn("[اطلاعات بازیابیشده از پایگاه دانش]", context)
|
||||
self.assertIn("kb text 1", context)
|
||||
self.assertIn("kb text 2", context)
|
||||
|
||||
@patch("rag.chat.search_with_query", return_value=[])
|
||||
@patch("rag.chat._rank_text_chunks_by_query", return_value=[])
|
||||
@patch("rag.chat.chunk_text", return_value=["farm chunk"])
|
||||
def test_build_chat_context_falls_back_to_full_farm_context(
|
||||
self,
|
||||
_mock_chunk_text,
|
||||
_mock_rank_text_chunks_by_query,
|
||||
_mock_search_with_query,
|
||||
):
|
||||
context = build_chat_context(
|
||||
query="رطوبت چقدر است؟",
|
||||
farm_uuid="farm-123",
|
||||
farm_details={"sensor_payload": {"sensor-7-1": {"soil_moisture": 30}}},
|
||||
)
|
||||
|
||||
self.assertEqual(context, "")
|
||||
@@ -0,0 +1,91 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from farm_data.models import SensorData
|
||||
from irrigation.models import IrrigationMethod
|
||||
from location_data.models import SoilLocation
|
||||
from plant.models import Plant
|
||||
from rag.services.fertilization import get_fertilization_recommendation
|
||||
from rag.services.irrigation import get_irrigation_recommendation
|
||||
from weather.models import WeatherForecast
|
||||
|
||||
|
||||
class RecommendationServiceDefaultsTests(TestCase):
|
||||
def setUp(self):
|
||||
self.location = SoilLocation.objects.create(
|
||||
latitude="35.700000",
|
||||
longitude="51.400000",
|
||||
farm_boundary={"type": "Polygon", "coordinates": []},
|
||||
)
|
||||
WeatherForecast.objects.create(
|
||||
location=self.location,
|
||||
forecast_date=date(2026, 4, 10),
|
||||
temperature_min=12.0,
|
||||
temperature_max=23.0,
|
||||
temperature_mean=18.0,
|
||||
)
|
||||
self.plant = Plant.objects.create(name="گوجهفرنگی")
|
||||
self.irrigation_method = IrrigationMethod.objects.create(name="آبیاری قطرهای")
|
||||
self.farm_uuid = uuid.uuid4()
|
||||
self.farm = SensorData.objects.create(
|
||||
farm_uuid=self.farm_uuid,
|
||||
center_location=self.location,
|
||||
irrigation_method=self.irrigation_method,
|
||||
sensor_payload={"sensor-7-1": {"soil_moisture": 30.0}},
|
||||
)
|
||||
self.farm.plants.set([self.plant])
|
||||
|
||||
@patch("rag.services.irrigation.calculate_forecast_water_needs", return_value=[])
|
||||
@patch("rag.services.irrigation.resolve_kc", return_value=0.9)
|
||||
@patch("rag.services.irrigation.resolve_crop_profile", return_value={})
|
||||
@patch("rag.services.irrigation.build_irrigation_method_text", return_value="method text")
|
||||
@patch("rag.services.irrigation.build_plant_text", return_value="plant text")
|
||||
@patch("rag.services.irrigation.build_rag_context", return_value="")
|
||||
@patch("rag.services.irrigation.get_chat_client")
|
||||
def test_irrigation_recommendation_uses_farm_relations_when_request_omits_names(
|
||||
self,
|
||||
mock_get_chat_client,
|
||||
mock_build_rag_context,
|
||||
mock_build_plant_text,
|
||||
mock_build_irrigation_method_text,
|
||||
_mock_resolve_crop_profile,
|
||||
_mock_resolve_kc,
|
||||
_mock_calculate_forecast_water_needs,
|
||||
):
|
||||
mock_response = Mock()
|
||||
mock_response.choices = [Mock(message=Mock(content='{"plan": {"frequencyPerWeek": 2}}'))]
|
||||
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
|
||||
|
||||
result = get_irrigation_recommendation(
|
||||
sensor_uuid=str(self.farm_uuid),
|
||||
growth_stage="میوهدهی",
|
||||
)
|
||||
|
||||
self.assertEqual(result["plan"]["frequencyPerWeek"], 2)
|
||||
mock_build_rag_context.assert_called_once()
|
||||
mock_build_plant_text.assert_called_once_with("گوجهفرنگی", "میوهدهی")
|
||||
mock_build_irrigation_method_text.assert_called_once_with("آبیاری قطرهای")
|
||||
|
||||
@patch("rag.services.fertilization.build_plant_text", return_value="plant text")
|
||||
@patch("rag.services.fertilization.build_rag_context", return_value="")
|
||||
@patch("rag.services.fertilization.get_chat_client")
|
||||
def test_fertilization_recommendation_uses_farm_plant_when_request_omits_name(
|
||||
self,
|
||||
mock_get_chat_client,
|
||||
_mock_build_rag_context,
|
||||
mock_build_plant_text,
|
||||
):
|
||||
mock_response = Mock()
|
||||
mock_response.choices = [Mock(message=Mock(content='{"plan": {"npkRatio": "20-20-20"}}'))]
|
||||
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
|
||||
|
||||
result = get_fertilization_recommendation(
|
||||
sensor_uuid=str(self.farm_uuid),
|
||||
growth_stage="رویشی",
|
||||
)
|
||||
|
||||
self.assertEqual(result["plan"]["npkRatio"], "20-20-20")
|
||||
mock_build_plant_text.assert_called_once_with("گوجهفرنگی", "رویشی")
|
||||
Reference in New Issue
Block a user