This commit is contained in:
2026-04-24 02:50:27 +03:30
parent 302124aa87
commit a76af4e766
20 changed files with 430 additions and 147 deletions
@@ -78,3 +78,65 @@ pH
6.8
بهترین جذب مواد مغذی را دارد.
فاصله کوددهی کلسیم و فسفر: کودهای حاوی کلسیم را هرگز با کودهای حاوی فسفر یا سولفات همزمان مخلوط نکنید (رسوب می‌کنند).
راهنمای کوددهی هویج
قاعده کلی برای هویج: هویج به نیتروژن (
𝑁
N
) کم تا متوسط، اما به فسفر (
𝑃
P
) و به ویژه پتاسیم (
𝐾
K
) بسیار بالایی نیاز دارد.
مراحل کوددهی:
آماده‌سازی خاک (قبل از کاشت): استفاده از کودهای پایه فسفر و پتاسیم. هشدار مهم: به هیچ وجه از کود دامی تازه استفاده نکنید! کود دامی باید کاملاً پوسیده باشد. کود حیوانی تازه باعث دو یا چند شاخه شدن هویج و ایجاد ریشه‌های مویی زائد می‌شود.
رشد رویشی (اوایل رشد): استفاده محدود از نیتروژن برای رشد برگ‌ها. نیتروژن بیش از حد باعث می‌شود گیاه تمام انرژی خود را صرف تولید برگ کند و ریشه (بخش خوراکی) نازک و کوچک بماند.
رشد و حجم گرفتن ریشه (اواسط تا اواخر رشد): استفاده از کودهای پتاس‌بالا (مانند سولوپتاس یا کودهای
12
12
36
121236
) برای افزایش سایز، بهبود رنگ، طعم شیرین‌تر و تردی هویج.
عناصر ریزمغذی کلیدی:
بُر (
𝐵
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",
),
),
]
+9
View File
@@ -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)
+29 -3
View File
@@ -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
View File
@@ -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,
}
+9 -1
View File
@@ -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,
+4
View File
@@ -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 -2
View File
@@ -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"),
]
+28 -63
View File
@@ -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(
sensor_uuid=sensor_uuid,
plant_name=plant_name,
growth_stage=growth_stage,
query=query,
)
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": 500, "msg": f"خطا در تولید توصیه کودهی: {exc}", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response(
{
"code": 202,
"msg": "تسک توصیه کودهی در صف قرار گرفت.",
"data": {
"task_id": task.id,
"status_url": f"/api/fertilization/recommend/{task.id}/status/",
},
},
status=status.HTTP_202_ACCEPTED,
)
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,
)
-2
View File
@@ -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"),
]
+29 -64
View File
@@ -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(
sensor_uuid=sensor_uuid,
plant_name=plant_name,
growth_stage=growth_stage,
irrigation_method_name=irrigation_method_name,
query=query,
)
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": 500, "msg": f"خطا در تولید توصیه آبیاری: {exc}", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response(
{
"code": 202,
"msg": "تسک توصیه آبیاری در صف قرار گرفت.",
"data": {
"task_id": task.id,
"status_url": f"/api/irrigation/recommend/{task.id}/status/",
},
},
status=status.HTTP_202_ACCEPTED,
)
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,
),
),
]
+5
View File
@@ -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,
+1
View File
@@ -15,6 +15,7 @@ class PlantSerializer(serializers.ModelSerializer):
"watering",
"soil",
"temperature",
"growth_stage",
"planting_season",
"harvest_time",
"spacing",
+1
View File
@@ -81,6 +81,7 @@ class PlantListCreateView(APIView):
"watering": "منظم، هفته‌ای ۲-۳ بار",
"soil": "لومی، غنی از مواد آلی",
"temperature": "۲۰-۳۰ درجه سانتی‌گراد",
"growth_stage": "رشد رویشی",
"planting_season": "بهار",
"harvest_time": "۷۰-۹۰ روز پس از کاشت",
"spacing": "۴۵-۶۰ سانتی‌متر",
+14 -2
View File
@@ -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:
+24 -6
View File
@@ -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,11 +107,18 @@ 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()
efficiency_percent = getattr(method, "water_efficiency_percent", None) if method else None
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,
latitude_deg=float(sensor.center_location.latitude),
@@ -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:
+52
View File
@@ -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, "")
+91
View File
@@ -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("گوجه‌فرنگی", "رویشی")