UPDATE
This commit is contained in:
@@ -78,3 +78,65 @@ pH
|
|||||||
6.8
|
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",
|
related_name="farm_data",
|
||||||
help_text="گیاهان مرتبط با این farm",
|
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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from location_data.serializers import SoilDepthDataSerializer
|
from location_data.serializers import SoilDepthDataSerializer
|
||||||
|
from irrigation.models import IrrigationMethod
|
||||||
|
from irrigation.serializers import IrrigationMethodSerializer
|
||||||
from plant.serializers import PlantSerializer
|
from plant.serializers import PlantSerializer
|
||||||
from weather.models import WeatherForecast
|
from weather.models import WeatherForecast
|
||||||
|
|
||||||
@@ -19,6 +21,11 @@ class SensorDataUpdateSerializer(serializers.Serializer):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text="لیست شناسه گیاهان مرتبط",
|
help_text="لیست شناسه گیاهان مرتبط",
|
||||||
)
|
)
|
||||||
|
irrigation_method_id = serializers.IntegerField(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
help_text="شناسه روش آبیاری مرتبط",
|
||||||
|
)
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
@@ -31,6 +38,7 @@ class SensorDataUpdateSerializer(serializers.Serializer):
|
|||||||
"sensor_key",
|
"sensor_key",
|
||||||
"sensor_payload",
|
"sensor_payload",
|
||||||
"plant_ids",
|
"plant_ids",
|
||||||
|
"irrigation_method_id",
|
||||||
}
|
}
|
||||||
flat_metrics = {
|
flat_metrics = {
|
||||||
key: value
|
key: value
|
||||||
@@ -71,10 +79,21 @@ class SensorDataUpdateSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
return value
|
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):
|
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(
|
raise serializers.ValidationError(
|
||||||
"حداقل یکی از sensor_payload یا plant_ids باید ارسال شود."
|
"حداقل یکی از sensor_payload یا plant_ids یا irrigation_method_id باید ارسال شود."
|
||||||
)
|
)
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
@@ -87,6 +106,11 @@ class SensorDataResponseSerializer(serializers.ModelSerializer):
|
|||||||
many=True,
|
many=True,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
)
|
)
|
||||||
|
irrigation_method_id = serializers.IntegerField(
|
||||||
|
source="irrigation_method.id",
|
||||||
|
read_only=True,
|
||||||
|
allow_null=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SensorData
|
model = SensorData
|
||||||
@@ -96,6 +120,7 @@ class SensorDataResponseSerializer(serializers.ModelSerializer):
|
|||||||
"weather_forecast_id",
|
"weather_forecast_id",
|
||||||
"sensor_payload",
|
"sensor_payload",
|
||||||
"plant_ids",
|
"plant_ids",
|
||||||
|
"irrigation_method_id",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
@@ -148,12 +173,13 @@ class FarmSoilPayloadSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
|
|
||||||
class FarmDetailSerializer(serializers.Serializer):
|
class FarmDetailSerializer(serializers.Serializer):
|
||||||
farm_uuid = serializers.UUIDField()
|
|
||||||
center_location = FarmCenterLocationSerializer()
|
center_location = FarmCenterLocationSerializer()
|
||||||
weather = WeatherForecastDetailSerializer(allow_null=True)
|
weather = WeatherForecastDetailSerializer(allow_null=True)
|
||||||
sensor_payload = serializers.JSONField()
|
sensor_payload = serializers.JSONField()
|
||||||
soil = FarmSoilPayloadSerializer()
|
soil = FarmSoilPayloadSerializer()
|
||||||
plant_ids = serializers.ListField(child=serializers.IntegerField())
|
plant_ids = serializers.ListField(child=serializers.IntegerField())
|
||||||
plants = PlantSerializer(many=True)
|
plants = PlantSerializer(many=True)
|
||||||
|
irrigation_method_id = serializers.IntegerField(allow_null=True)
|
||||||
|
irrigation_method = IrrigationMethodSerializer(allow_null=True)
|
||||||
created_at = serializers.DateTimeField()
|
created_at = serializers.DateTimeField()
|
||||||
updated_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.models import SoilLocation
|
||||||
from location_data.serializers import SoilDepthDataSerializer
|
from location_data.serializers import SoilDepthDataSerializer
|
||||||
from location_data.tasks import fetch_soil_data_for_coordinates
|
from location_data.tasks import fetch_soil_data_for_coordinates
|
||||||
|
from irrigation.serializers import IrrigationMethodSerializer
|
||||||
from plant.serializers import PlantSerializer
|
from plant.serializers import PlantSerializer
|
||||||
from weather.services import update_weather_for_location
|
from weather.services import update_weather_for_location
|
||||||
from weather.models import WeatherForecast
|
from weather.models import WeatherForecast
|
||||||
@@ -25,7 +26,11 @@ class ExternalDataSyncError(Exception):
|
|||||||
|
|
||||||
def get_farm_details(farm_uuid: str):
|
def get_farm_details(farm_uuid: str):
|
||||||
farm = (
|
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")
|
.prefetch_related("plants", "center_location__depths")
|
||||||
.filter(farm_uuid=farm_uuid)
|
.filter(farm_uuid=farm_uuid)
|
||||||
.first()
|
.first()
|
||||||
@@ -53,7 +58,6 @@ def get_farm_details(farm_uuid: str):
|
|||||||
metric_sources[key] = "sensor"
|
metric_sources[key] = "sensor"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"farm_uuid": farm.farm_uuid,
|
|
||||||
"center_location": {
|
"center_location": {
|
||||||
"id": center_location.id,
|
"id": center_location.id,
|
||||||
"lat": center_location.latitude,
|
"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)),
|
"plant_ids": list(farm.plants.values_list("id", flat=True)),
|
||||||
"plants": PlantSerializer(farm.plants.all(), many=True).data,
|
"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,
|
"created_at": farm.created_at,
|
||||||
"updated_at": farm.updated_at,
|
"updated_at": farm.updated_at,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from rest_framework.test import APIClient
|
|||||||
|
|
||||||
from location_data.models import SoilDepthData, SoilLocation
|
from location_data.models import SoilDepthData, SoilLocation
|
||||||
from farm_data.models import SensorData
|
from farm_data.models import SensorData
|
||||||
|
from irrigation.models import IrrigationMethod
|
||||||
from plant.models import Plant
|
from plant.models import Plant
|
||||||
from weather.models import WeatherForecast
|
from weather.models import WeatherForecast
|
||||||
|
|
||||||
@@ -58,11 +59,13 @@ class FarmDetailApiTests(TestCase):
|
|||||||
)
|
)
|
||||||
self.plant1 = Plant.objects.create(name="گوجهفرنگی")
|
self.plant1 = Plant.objects.create(name="گوجهفرنگی")
|
||||||
self.plant2 = Plant.objects.create(name="خیار")
|
self.plant2 = Plant.objects.create(name="خیار")
|
||||||
|
self.irrigation_method = IrrigationMethod.objects.create(name="آبیاری قطرهای")
|
||||||
self.farm_uuid = uuid.uuid4()
|
self.farm_uuid = uuid.uuid4()
|
||||||
self.farm = SensorData.objects.create(
|
self.farm = SensorData.objects.create(
|
||||||
farm_uuid=self.farm_uuid,
|
farm_uuid=self.farm_uuid,
|
||||||
center_location=self.location,
|
center_location=self.location,
|
||||||
weather_forecast=self.weather,
|
weather_forecast=self.weather,
|
||||||
|
irrigation_method=self.irrigation_method,
|
||||||
sensor_payload={
|
sensor_payload={
|
||||||
"sensor-7-1": {
|
"sensor-7-1": {
|
||||||
"soil_moisture": 33.5,
|
"soil_moisture": 33.5,
|
||||||
@@ -78,7 +81,7 @@ class FarmDetailApiTests(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
payload = response.json()["data"]
|
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["center_location"]["id"], self.location.id)
|
||||||
self.assertEqual(payload["weather"]["id"], self.weather.id)
|
self.assertEqual(payload["weather"]["id"], self.weather.id)
|
||||||
self.assertEqual(
|
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.plant1.id]["name"], self.plant1.name)
|
||||||
self.assertEqual(returned_plants[self.plant2.id]["name"], self.plant2.name)
|
self.assertEqual(returned_plants[self.plant2.id]["name"], self.plant2.name)
|
||||||
self.assertIn("light", returned_plants[self.plant1.id])
|
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):
|
def test_returns_404_when_farm_is_missing(self):
|
||||||
response = self.client.get(f"/api/farm-data/{uuid.uuid4()}/detail/")
|
response = self.client.get(f"/api/farm-data/{uuid.uuid4()}/detail/")
|
||||||
@@ -123,6 +128,7 @@ class FarmDataUpsertApiTests(TestCase):
|
|||||||
temperature_max=24.0,
|
temperature_max=24.0,
|
||||||
temperature_mean=17.5,
|
temperature_mean=17.5,
|
||||||
)
|
)
|
||||||
|
self.irrigation_method = IrrigationMethod.objects.create(name="بارانی")
|
||||||
|
|
||||||
def test_post_creates_farm_data_with_explicit_farm_uuid(self):
|
def test_post_creates_farm_data_with_explicit_farm_uuid(self):
|
||||||
farm_uuid = uuid.uuid4()
|
farm_uuid = uuid.uuid4()
|
||||||
@@ -138,6 +144,7 @@ class FarmDataUpsertApiTests(TestCase):
|
|||||||
"nitrogen": 18.0,
|
"nitrogen": 18.0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"irrigation_method_id": self.irrigation_method.id,
|
||||||
},
|
},
|
||||||
format="json",
|
format="json",
|
||||||
)
|
)
|
||||||
@@ -150,6 +157,7 @@ class FarmDataUpsertApiTests(TestCase):
|
|||||||
farm = SensorData.objects.get(farm_uuid=farm_uuid)
|
farm = SensorData.objects.get(farm_uuid=farm_uuid)
|
||||||
self.assertEqual(farm.center_location_id, self.location.id)
|
self.assertEqual(farm.center_location_id, self.location.id)
|
||||||
self.assertEqual(farm.weather_forecast_id, self.weather.id)
|
self.assertEqual(farm.weather_forecast_id, self.weather.id)
|
||||||
|
self.assertEqual(farm.irrigation_method_id, self.irrigation_method.id)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
farm.sensor_payload["sensor-7-1"]["soil_moisture"],
|
farm.sensor_payload["sensor-7-1"]["soil_moisture"],
|
||||||
31.2,
|
31.2,
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ class FarmDataUpsertView(APIView):
|
|||||||
farm_uuid = serializer.validated_data["farm_uuid"]
|
farm_uuid = serializer.validated_data["farm_uuid"]
|
||||||
farm_boundary = serializer.validated_data["farm_boundary"]
|
farm_boundary = serializer.validated_data["farm_boundary"]
|
||||||
plant_ids = serializer.validated_data.get("plant_ids")
|
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", {})
|
sensor_payload = serializer.validated_data.get("sensor_payload", {})
|
||||||
try:
|
try:
|
||||||
center_location = resolve_center_location_from_boundary(farm_boundary)
|
center_location = resolve_center_location_from_boundary(farm_boundary)
|
||||||
@@ -210,12 +211,15 @@ class FarmDataUpsertView(APIView):
|
|||||||
|
|
||||||
farm_data.center_location = center_location
|
farm_data.center_location = center_location
|
||||||
farm_data.weather_forecast = weather_forecast
|
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:
|
if not created:
|
||||||
farm_data.save(
|
farm_data.save(
|
||||||
update_fields=[
|
update_fields=[
|
||||||
"center_location",
|
"center_location",
|
||||||
"weather_forecast",
|
"weather_forecast",
|
||||||
"sensor_payload",
|
"sensor_payload",
|
||||||
|
"irrigation_method",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import FertilizationRecommendView, FertilizationRecommendStatusView
|
from .views import FertilizationRecommendView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("recommend/", FertilizationRecommendView.as_view(), name="fertilization-recommend"),
|
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 (
|
from config.openapi import (
|
||||||
build_envelope_serializer,
|
build_envelope_serializer,
|
||||||
build_response,
|
build_response,
|
||||||
build_task_queue_data_serializer,
|
|
||||||
build_task_status_data_serializer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .serializers import FertilizationRecommendRequestSerializer
|
from .serializers import FertilizationRecommendRequestSerializer
|
||||||
|
|
||||||
|
|
||||||
FertilizationQueueResponseSerializer = build_envelope_serializer(
|
|
||||||
"FertilizationQueueResponseSerializer",
|
|
||||||
build_task_queue_data_serializer("FertilizationQueueDataSerializer"),
|
|
||||||
)
|
|
||||||
FertilizationValidationErrorSerializer = build_envelope_serializer(
|
FertilizationValidationErrorSerializer = build_envelope_serializer(
|
||||||
"FertilizationValidationErrorSerializer",
|
"FertilizationValidationErrorSerializer",
|
||||||
data_required=False,
|
data_required=False,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
)
|
)
|
||||||
FertilizationStatusResponseSerializer = build_envelope_serializer(
|
FertilizationResponseSerializer = build_envelope_serializer(
|
||||||
"FertilizationStatusResponseSerializer",
|
"FertilizationResponseSerializer",
|
||||||
build_task_status_data_serializer("FertilizationStatusDataSerializer"),
|
data_schema=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FertilizationRecommendView(APIView):
|
class FertilizationRecommendView(APIView):
|
||||||
"""
|
"""
|
||||||
توصیه کودهی با Celery.
|
توصیه کودهی به صورت مستقیم.
|
||||||
POST با sensor_uuid، plant_name، growth_stage.
|
POST با sensor_uuid، plant_name، growth_stage.
|
||||||
اطلاعات گیاه از plant app دریافت میشود.
|
اطلاعات گیاه از plant app دریافت میشود.
|
||||||
نیازی به دریافت نوع آبیاری نیست.
|
نیازی به دریافت نوع آبیاری نیست.
|
||||||
@@ -44,21 +38,25 @@ class FertilizationRecommendView(APIView):
|
|||||||
tags=["Fertilization Recommendation"],
|
tags=["Fertilization Recommendation"],
|
||||||
summary="درخواست توصیه کودهی",
|
summary="درخواست توصیه کودهی",
|
||||||
description=(
|
description=(
|
||||||
"دادههای سنسور و گیاه را دریافت کرده و یک تسک Celery "
|
"دادههای سنسور و گیاه را دریافت کرده و "
|
||||||
"برای تولید توصیه کودهی در صف قرار میدهد. "
|
"توصیه کودهی را مستقیم برمیگرداند. "
|
||||||
"اطلاعات گیاه از جدول Plant بارگذاری میشود. "
|
"اطلاعات گیاه از جدول Plant بارگذاری میشود. "
|
||||||
"محاسبات مربوط به نیاز آبی در این endpoint انجام نمیشود و مستقل از توصیه کودهی است."
|
"محاسبات مربوط به نیاز آبی در این endpoint انجام نمیشود و مستقل از توصیه کودهی است."
|
||||||
),
|
),
|
||||||
request=FertilizationRecommendRequestSerializer,
|
request=FertilizationRecommendRequestSerializer,
|
||||||
responses={
|
responses={
|
||||||
202: build_response(
|
200: build_response(
|
||||||
FertilizationQueueResponseSerializer,
|
FertilizationResponseSerializer,
|
||||||
"تسک توصیه کودهی در صف قرار گرفت.",
|
"توصیه کودهی با موفقیت تولید شد.",
|
||||||
),
|
),
|
||||||
400: build_response(
|
400: build_response(
|
||||||
FertilizationValidationErrorSerializer,
|
FertilizationValidationErrorSerializer,
|
||||||
"پارامتر ورودی نامعتبر است.",
|
"پارامتر ورودی نامعتبر است.",
|
||||||
),
|
),
|
||||||
|
500: build_response(
|
||||||
|
FertilizationValidationErrorSerializer,
|
||||||
|
"خطا در تولید توصیه کودهی.",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
examples=[
|
examples=[
|
||||||
OpenApiExample(
|
OpenApiExample(
|
||||||
@@ -73,7 +71,7 @@ class FertilizationRecommendView(APIView):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
from rag.tasks import fertilization_recommendation_task
|
from rag.services.fertilization import get_fertilization_recommendation
|
||||||
|
|
||||||
serializer = FertilizationRecommendRequestSerializer(data=request.data)
|
serializer = FertilizationRecommendRequestSerializer(data=request.data)
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
@@ -88,53 +86,20 @@ class FertilizationRecommendView(APIView):
|
|||||||
growth_stage = validated.get("growth_stage")
|
growth_stage = validated.get("growth_stage")
|
||||||
query = validated.get("query")
|
query = validated.get("query")
|
||||||
|
|
||||||
task = fertilization_recommendation_task.delay(
|
try:
|
||||||
|
result = get_fertilization_recommendation(
|
||||||
sensor_uuid=sensor_uuid,
|
sensor_uuid=sensor_uuid,
|
||||||
plant_name=plant_name,
|
plant_name=plant_name,
|
||||||
growth_stage=growth_stage,
|
growth_stage=growth_stage,
|
||||||
query=query,
|
query=query,
|
||||||
)
|
)
|
||||||
|
except Exception as exc:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"code": 500, "msg": f"خطا در تولید توصیه کودهی: {exc}", "data": None},
|
||||||
"code": 202,
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
"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(
|
return Response(
|
||||||
{"code": 200, "msg": "success", "data": data},
|
{"code": 200, "msg": "success", "data": result},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ from .views import (
|
|||||||
IrrigationMethodDetailView,
|
IrrigationMethodDetailView,
|
||||||
IrrigationMethodListCreateView,
|
IrrigationMethodListCreateView,
|
||||||
IrrigationRecommendView,
|
IrrigationRecommendView,
|
||||||
IrrigationRecommendStatusView,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", IrrigationMethodListCreateView.as_view(), name="irrigation-list-create"),
|
path("", IrrigationMethodListCreateView.as_view(), name="irrigation-list-create"),
|
||||||
path("<int:pk>/", IrrigationMethodDetailView.as_view(), name="irrigation-detail"),
|
path("<int:pk>/", IrrigationMethodDetailView.as_view(), name="irrigation-detail"),
|
||||||
path("recommend/", IrrigationRecommendView.as_view(), name="irrigation-recommend"),
|
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 (
|
from config.openapi import (
|
||||||
build_envelope_serializer,
|
build_envelope_serializer,
|
||||||
build_response,
|
build_response,
|
||||||
build_task_queue_data_serializer,
|
|
||||||
build_task_status_data_serializer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import IrrigationMethod
|
from .models import IrrigationMethod
|
||||||
@@ -31,13 +29,9 @@ IrrigationValidationErrorSerializer = build_envelope_serializer(
|
|||||||
data_required=False,
|
data_required=False,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
)
|
)
|
||||||
IrrigationQueueResponseSerializer = build_envelope_serializer(
|
IrrigationRecommendResponseSerializer = build_envelope_serializer(
|
||||||
"IrrigationQueueResponseSerializer",
|
"IrrigationRecommendResponseSerializer",
|
||||||
build_task_queue_data_serializer("IrrigationQueueDataSerializer"),
|
data_schema=None,
|
||||||
)
|
|
||||||
IrrigationStatusResponseSerializer = build_envelope_serializer(
|
|
||||||
"IrrigationStatusResponseSerializer",
|
|
||||||
build_task_status_data_serializer("IrrigationStatusDataSerializer"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +106,7 @@ class IrrigationMethodListCreateView(APIView):
|
|||||||
|
|
||||||
class IrrigationRecommendView(APIView):
|
class IrrigationRecommendView(APIView):
|
||||||
"""
|
"""
|
||||||
توصیه آبیاری با Celery.
|
توصیه آبیاری به صورت مستقیم.
|
||||||
POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name.
|
POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name.
|
||||||
اطلاعات گیاه از plant app و روش آبیاری از irrigation app دریافت میشود.
|
اطلاعات گیاه از plant app و روش آبیاری از irrigation app دریافت میشود.
|
||||||
"""
|
"""
|
||||||
@@ -121,21 +115,25 @@ class IrrigationRecommendView(APIView):
|
|||||||
tags=["Irrigation Recommendation"],
|
tags=["Irrigation Recommendation"],
|
||||||
summary="درخواست توصیه آبیاری",
|
summary="درخواست توصیه آبیاری",
|
||||||
description=(
|
description=(
|
||||||
"دادههای سنسور، گیاه و روش آبیاری را دریافت کرده و یک تسک Celery "
|
"دادههای سنسور، گیاه و روش آبیاری را دریافت کرده و "
|
||||||
"برای تولید توصیه آبیاری در صف قرار میدهد. "
|
"توصیه آبیاری را مستقیم برمیگرداند. "
|
||||||
"اطلاعات گیاه از جدول Plant و روش آبیاری از جدول IrrigationMethod بارگذاری میشود. "
|
"اطلاعات گیاه از جدول Plant و روش آبیاری از جدول IrrigationMethod بارگذاری میشود. "
|
||||||
"محاسبات ET₀ و ETc با مدل FAO-56 در بکاند انجام میشود و مدل زبانی فقط توضیح برنامه آبیاری را تولید میکند."
|
"محاسبات ET₀ و ETc با مدل FAO-56 در بکاند انجام میشود و مدل زبانی فقط توضیح برنامه آبیاری را تولید میکند."
|
||||||
),
|
),
|
||||||
request=IrrigationRecommendRequestSerializer,
|
request=IrrigationRecommendRequestSerializer,
|
||||||
responses={
|
responses={
|
||||||
202: build_response(
|
200: build_response(
|
||||||
IrrigationQueueResponseSerializer,
|
IrrigationRecommendResponseSerializer,
|
||||||
"تسک توصیه آبیاری در صف قرار گرفت.",
|
"توصیه آبیاری با موفقیت تولید شد.",
|
||||||
),
|
),
|
||||||
400: build_response(
|
400: build_response(
|
||||||
IrrigationValidationErrorSerializer,
|
IrrigationValidationErrorSerializer,
|
||||||
"پارامتر ورودی نامعتبر است.",
|
"پارامتر ورودی نامعتبر است.",
|
||||||
),
|
),
|
||||||
|
500: build_response(
|
||||||
|
IrrigationValidationErrorSerializer,
|
||||||
|
"خطا در تولید توصیه آبیاری.",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
examples=[
|
examples=[
|
||||||
OpenApiExample(
|
OpenApiExample(
|
||||||
@@ -151,7 +149,7 @@ class IrrigationRecommendView(APIView):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
from rag.tasks import irrigation_recommendation_task
|
from rag.services.irrigation import get_irrigation_recommendation
|
||||||
|
|
||||||
serializer = IrrigationRecommendRequestSerializer(data=request.data)
|
serializer = IrrigationRecommendRequestSerializer(data=request.data)
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
@@ -167,55 +165,22 @@ class IrrigationRecommendView(APIView):
|
|||||||
irrigation_method_name = validated.get("irrigation_method_name")
|
irrigation_method_name = validated.get("irrigation_method_name")
|
||||||
query = validated.get("query")
|
query = validated.get("query")
|
||||||
|
|
||||||
task = irrigation_recommendation_task.delay(
|
try:
|
||||||
|
result = get_irrigation_recommendation(
|
||||||
sensor_uuid=sensor_uuid,
|
sensor_uuid=sensor_uuid,
|
||||||
plant_name=plant_name,
|
plant_name=plant_name,
|
||||||
growth_stage=growth_stage,
|
growth_stage=growth_stage,
|
||||||
irrigation_method_name=irrigation_method_name,
|
irrigation_method_name=irrigation_method_name,
|
||||||
query=query,
|
query=query,
|
||||||
)
|
)
|
||||||
|
except Exception as exc:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{"code": 500, "msg": f"خطا در تولید توصیه آبیاری: {exc}", "data": None},
|
||||||
"code": 202,
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
"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(
|
return Response(
|
||||||
{"code": 200, "msg": "success", "data": data},
|
{"code": 200, "msg": "success", "data": result},
|
||||||
status=status.HTTP_200_OK,
|
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,
|
blank=True,
|
||||||
help_text="دمای مناسب",
|
help_text="دمای مناسب",
|
||||||
)
|
)
|
||||||
|
growth_stage = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
help_text="مرحله رشد",
|
||||||
|
)
|
||||||
planting_season = models.CharField(
|
planting_season = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class PlantSerializer(serializers.ModelSerializer):
|
|||||||
"watering",
|
"watering",
|
||||||
"soil",
|
"soil",
|
||||||
"temperature",
|
"temperature",
|
||||||
|
"growth_stage",
|
||||||
"planting_season",
|
"planting_season",
|
||||||
"harvest_time",
|
"harvest_time",
|
||||||
"spacing",
|
"spacing",
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class PlantListCreateView(APIView):
|
|||||||
"watering": "منظم، هفتهای ۲-۳ بار",
|
"watering": "منظم، هفتهای ۲-۳ بار",
|
||||||
"soil": "لومی، غنی از مواد آلی",
|
"soil": "لومی، غنی از مواد آلی",
|
||||||
"temperature": "۲۰-۳۰ درجه سانتیگراد",
|
"temperature": "۲۰-۳۰ درجه سانتیگراد",
|
||||||
|
"growth_stage": "رشد رویشی",
|
||||||
"planting_season": "بهار",
|
"planting_season": "بهار",
|
||||||
"harvest_time": "۷۰-۹۰ روز پس از کاشت",
|
"harvest_time": "۷۰-۹۰ روز پس از کاشت",
|
||||||
"spacing": "۴۵-۶۰ سانتیمتر",
|
"spacing": "۴۵-۶۰ سانتیمتر",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from farm_data.models import SensorData
|
||||||
from rag.api_provider import get_chat_client
|
from rag.api_provider import get_chat_client
|
||||||
from rag.chat import (
|
from rag.chat import (
|
||||||
_complete_audit_log,
|
_complete_audit_log,
|
||||||
@@ -78,13 +79,24 @@ def get_fertilization_recommendation(
|
|||||||
|
|
||||||
user_query = query or "توصیه کودهی برای مزرعه من چیست؟"
|
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(
|
context = build_rag_context(
|
||||||
user_query, sensor_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID,
|
user_query, sensor_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
extra_parts: list[str] = []
|
extra_parts: list[str] = []
|
||||||
if plant_name and growth_stage:
|
if resolved_plant_name and growth_stage:
|
||||||
plant_text = build_plant_text(plant_name, growth_stage)
|
plant_text = build_plant_text(resolved_plant_name, growth_stage)
|
||||||
if plant_text:
|
if plant_text:
|
||||||
extra_parts.append("[اطلاعات گیاه]\n" + plant_text)
|
extra_parts.append("[اطلاعات گیاه]\n" + plant_text)
|
||||||
if extra_parts:
|
if extra_parts:
|
||||||
|
|||||||
@@ -83,12 +83,20 @@ def get_irrigation_recommendation(
|
|||||||
|
|
||||||
user_query = query or "توصیه آبیاری برای مزرعه من چیست؟"
|
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
|
plant = None
|
||||||
|
resolved_plant_name = plant_name
|
||||||
if sensor is not None and plant_name:
|
if sensor is not None and plant_name:
|
||||||
plant = sensor.plants.filter(name=plant_name).first()
|
plant = sensor.plants.filter(name=plant_name).first()
|
||||||
elif sensor is not None:
|
elif sensor is not None:
|
||||||
plant = sensor.plants.first()
|
plant = sensor.plants.first()
|
||||||
|
if plant is not None:
|
||||||
|
resolved_plant_name = plant.name
|
||||||
crop_profile = resolve_crop_profile(plant, growth_stage=growth_stage)
|
crop_profile = resolve_crop_profile(plant, growth_stage=growth_stage)
|
||||||
active_kc = resolve_kc(crop_profile, growth_stage=growth_stage)
|
active_kc = resolve_kc(crop_profile, growth_stage=growth_stage)
|
||||||
forecasts = []
|
forecasts = []
|
||||||
@@ -99,10 +107,17 @@ def get_irrigation_recommendation(
|
|||||||
.order_by("forecast_date")[:7]
|
.order_by("forecast_date")[:7]
|
||||||
)
|
)
|
||||||
efficiency_percent = None
|
efficiency_percent = None
|
||||||
|
resolved_irrigation_method_name = irrigation_method_name
|
||||||
|
method = None
|
||||||
if irrigation_method_name:
|
if irrigation_method_name:
|
||||||
from irrigation.models import IrrigationMethod
|
from irrigation.models import IrrigationMethod
|
||||||
|
|
||||||
method = IrrigationMethod.objects.filter(name=irrigation_method_name).first()
|
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
|
efficiency_percent = getattr(method, "water_efficiency_percent", None) if method else None
|
||||||
daily_water_needs = calculate_forecast_water_needs(
|
daily_water_needs = calculate_forecast_water_needs(
|
||||||
forecasts=forecasts,
|
forecasts=forecasts,
|
||||||
@@ -117,12 +132,15 @@ def get_irrigation_recommendation(
|
|||||||
)
|
)
|
||||||
|
|
||||||
extra_parts: list[str] = []
|
extra_parts: list[str] = []
|
||||||
if plant_name and growth_stage:
|
resolved_irrigation_method_name = irrigation_method_name or (
|
||||||
plant_text = build_plant_text(plant_name, growth_stage)
|
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:
|
if plant_text:
|
||||||
extra_parts.append("[اطلاعات گیاه]\n" + plant_text)
|
extra_parts.append("[اطلاعات گیاه]\n" + plant_text)
|
||||||
if irrigation_method_name:
|
if resolved_irrigation_method_name:
|
||||||
method_text = build_irrigation_method_text(irrigation_method_name)
|
method_text = build_irrigation_method_text(resolved_irrigation_method_name)
|
||||||
if method_text:
|
if method_text:
|
||||||
extra_parts.append("[روش آبیاری انتخابی]\n" + method_text)
|
extra_parts.append("[روش آبیاری انتخابی]\n" + method_text)
|
||||||
if daily_water_needs:
|
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