338 lines
11 KiB
Python
338 lines
11 KiB
Python
from django.db import models
|
|
|
|
|
|
DEFAULT_SENSOR_KEY = "sensor-7-1"
|
|
DEFAULT_SENSOR_DATA_TYPE = "float"
|
|
|
|
|
|
class SensorPayloadMixin:
|
|
"""دسترسی سازگار به مقادیر سنسور از payload پویا."""
|
|
|
|
sensor_payload: dict
|
|
|
|
def _payload(self) -> dict:
|
|
if isinstance(self.sensor_payload, dict):
|
|
return self.sensor_payload
|
|
return {}
|
|
|
|
def get_sensor_block(self, sensor_key: str | None = None) -> dict:
|
|
payload = self._payload()
|
|
if sensor_key:
|
|
block = payload.get(sensor_key, {})
|
|
return block if isinstance(block, dict) else {}
|
|
|
|
for _sensor_key, block in self.iter_sensor_blocks():
|
|
return block
|
|
return {}
|
|
|
|
def iter_sensor_blocks(self):
|
|
for sensor_key, block in self._payload().items():
|
|
if isinstance(block, dict):
|
|
yield sensor_key, block
|
|
|
|
def get_metric(self, metric_name: str, sensor_key: str | None = None):
|
|
block = self.get_sensor_block(sensor_key)
|
|
if metric_name in block:
|
|
return block.get(metric_name)
|
|
|
|
for _candidate_key, candidate in self.iter_sensor_blocks():
|
|
if metric_name in candidate:
|
|
return candidate.get(metric_name)
|
|
return None
|
|
|
|
def get_sensor_keys(self) -> list[str]:
|
|
return [sensor_key for sensor_key, _block in self.iter_sensor_blocks()]
|
|
|
|
def get_all_metrics(self) -> dict[str, dict]:
|
|
return {
|
|
sensor_key: dict(block)
|
|
for sensor_key, block in self.iter_sensor_blocks()
|
|
}
|
|
|
|
@property
|
|
def soil_moisture(self):
|
|
return self.get_metric("soil_moisture")
|
|
|
|
@property
|
|
def soil_temperature(self):
|
|
return self.get_metric("soil_temperature")
|
|
|
|
@property
|
|
def soil_ph(self):
|
|
return self.get_metric("soil_ph")
|
|
|
|
@property
|
|
def electrical_conductivity(self):
|
|
return self.get_metric("electrical_conductivity")
|
|
|
|
@property
|
|
def nitrogen(self):
|
|
return self.get_metric("nitrogen")
|
|
|
|
@property
|
|
def phosphorus(self):
|
|
return self.get_metric("phosphorus")
|
|
|
|
@property
|
|
def potassium(self):
|
|
return self.get_metric("potassium")
|
|
|
|
|
|
class SensorData(SensorPayloadMixin, models.Model):
|
|
"""
|
|
دادههای مزرعه/سنسور برای مرکز زمین.
|
|
مقادیر سنسورها بهصورت JSON ذخیره میشوند تا بتوان چند نوع سنسور
|
|
و پارامترهای دلخواه را در یک رکورد نگه داشت.
|
|
نمونه:
|
|
{
|
|
"sensor-7-1": {
|
|
"soil_moisture": 22.4,
|
|
"soil_temperature": 18.1
|
|
},
|
|
"leaf-sensor": {
|
|
"leaf_wetness": 11
|
|
}
|
|
}
|
|
"""
|
|
|
|
farm_uuid = models.UUIDField(
|
|
primary_key=True,
|
|
editable=False,
|
|
help_text="شناسه یکتای farm که از API دریافت میشود",
|
|
)
|
|
center_location = models.ForeignKey(
|
|
"location_data.SoilLocation",
|
|
on_delete=models.CASCADE,
|
|
related_name="farm_data",
|
|
db_column="center_location_id",
|
|
help_text="مرکز زمین مرتبط از جدول location_data.SoilLocation",
|
|
)
|
|
weather_forecast = models.ForeignKey(
|
|
"weather.WeatherForecast",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="farm_data_entries",
|
|
db_column="weather_forecast_id",
|
|
help_text="رکورد آب وهوای مرتبط با مرکز زمین",
|
|
)
|
|
sensor_payload = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text='اطلاعات سنسورها در فرمت {"sensor-7-1": {...}}',
|
|
)
|
|
plants = models.ManyToManyField(
|
|
"plant.Plant",
|
|
blank=True,
|
|
db_table="farm_data_sensordata_plants",
|
|
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)
|
|
|
|
class Meta:
|
|
db_table = "farm_data_sensordata"
|
|
ordering = ["-updated_at"]
|
|
verbose_name = "farm-data"
|
|
verbose_name_plural = "farm-data"
|
|
|
|
def __str__(self):
|
|
return (
|
|
f"SensorData({self.farm_uuid}, center_location={self.center_location_id}, "
|
|
f"weather_forecast={self.weather_forecast_id})"
|
|
)
|
|
|
|
@property
|
|
def location(self):
|
|
return self.center_location
|
|
|
|
@location.setter
|
|
def location(self, value):
|
|
self.center_location = value
|
|
|
|
@property
|
|
def location_id(self):
|
|
return self.center_location_id
|
|
|
|
@property
|
|
def plant_snapshots(self):
|
|
return [assignment.plant for assignment in self.plant_assignments.select_related("plant").order_by("position", "id")]
|
|
|
|
|
|
class PlantCatalogSnapshot(models.Model):
|
|
"""
|
|
کپی خواندنی از کاتالوگ گیاه Backend برای مصرف ماژولهای AI.
|
|
"""
|
|
|
|
backend_plant_id = models.PositiveIntegerField(
|
|
unique=True,
|
|
db_index=True,
|
|
help_text="شناسه گیاه در Backend/plants",
|
|
)
|
|
name = models.CharField(max_length=255, db_index=True)
|
|
slug = models.SlugField(max_length=255, blank=True, default="")
|
|
icon = models.CharField(max_length=255, blank=True, default="leaf")
|
|
description = models.TextField(blank=True, default="")
|
|
metadata = models.JSONField(default=dict, blank=True)
|
|
light = models.CharField(max_length=255, blank=True, default="")
|
|
watering = models.CharField(max_length=255, blank=True, default="")
|
|
soil = models.CharField(max_length=255, blank=True, default="")
|
|
temperature = models.CharField(max_length=255, blank=True, default="")
|
|
growth_stage = models.CharField(max_length=255, blank=True, default="")
|
|
growth_stages = models.JSONField(blank=True, default=list)
|
|
planting_season = models.CharField(max_length=255, blank=True, default="")
|
|
harvest_time = models.CharField(max_length=255, blank=True, default="")
|
|
spacing = models.CharField(max_length=255, blank=True, default="")
|
|
fertilizer = models.CharField(max_length=255, blank=True, default="")
|
|
health_profile = models.JSONField(default=dict, blank=True)
|
|
irrigation_profile = models.JSONField(default=dict, blank=True)
|
|
growth_profile = models.JSONField(default=dict, blank=True)
|
|
is_active = models.BooleanField(default=True)
|
|
source_updated_at = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="updated_at رکورد canonical در Backend",
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = "farm_data_plantcatalogsnapshot"
|
|
ordering = ["name", "backend_plant_id"]
|
|
verbose_name = "plant catalog snapshot"
|
|
verbose_name_plural = "plant catalog snapshots"
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.backend_plant_id})"
|
|
|
|
|
|
class FarmPlantAssignment(models.Model):
|
|
"""
|
|
رابطه مزرعه با snapshot گیاه برای read-model هوش مصنوعی.
|
|
"""
|
|
|
|
farm = models.ForeignKey(
|
|
SensorData,
|
|
on_delete=models.CASCADE,
|
|
related_name="plant_assignments",
|
|
db_column="farm_uuid",
|
|
)
|
|
plant = models.ForeignKey(
|
|
PlantCatalogSnapshot,
|
|
on_delete=models.CASCADE,
|
|
related_name="farm_assignments",
|
|
)
|
|
position = models.PositiveIntegerField(default=0)
|
|
stage = models.CharField(max_length=64, blank=True, default="")
|
|
metadata = models.JSONField(default=dict, blank=True)
|
|
assigned_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = "farm_data_farmplantassignment"
|
|
ordering = ["position", "id"]
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["farm", "plant"],
|
|
name="farm_data_unique_farm_plant_assignment",
|
|
)
|
|
]
|
|
verbose_name = "farm plant assignment"
|
|
verbose_name_plural = "farm plant assignments"
|
|
|
|
def __str__(self):
|
|
return f"{self.farm_id} -> {self.plant_id}"
|
|
|
|
|
|
class SensorParameter(models.Model):
|
|
"""
|
|
تعریف پارامترهای سنسور برای هر نوع سنسور.
|
|
با این ساختار میتوان برای sensor-7-1 یا هر سنسور جدید،
|
|
پارامترهای اختصاصی تعریف کرد.
|
|
"""
|
|
|
|
sensor_key = models.CharField(
|
|
max_length=64,
|
|
db_index=True,
|
|
default=DEFAULT_SENSOR_KEY,
|
|
help_text='کلید سنسور داخل JSON مثل "sensor-7-1" یا "leaf-sensor"',
|
|
)
|
|
code = models.CharField(
|
|
max_length=64,
|
|
db_index=True,
|
|
help_text="کد پارامتر (مثلاً soil_moisture)",
|
|
)
|
|
name_fa = models.CharField(max_length=128, help_text="نام فارسی")
|
|
unit = models.CharField(max_length=32, blank=True, help_text="واحد اندازهگیری")
|
|
data_type = models.CharField(
|
|
max_length=32,
|
|
default=DEFAULT_SENSOR_DATA_TYPE,
|
|
help_text="نوع داده پارامتر مثل float, int, string, bool",
|
|
)
|
|
metadata = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text="اطلاعات تکمیلی پارامتر مثل بازه مجاز، توضیح یا تنظیمات UI",
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = "farm_data_sensorparameter"
|
|
ordering = ["sensor_key", "code"]
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["sensor_key", "code"],
|
|
name="sensor_parameter_unique_sensor_code",
|
|
)
|
|
]
|
|
verbose_name = "پارامتر سنسور"
|
|
verbose_name_plural = "پارامترهای سنسور"
|
|
|
|
def __str__(self):
|
|
return f"{self.sensor_key}.{self.code} ({self.name_fa})"
|
|
|
|
|
|
class ParameterUpdateLog(models.Model):
|
|
"""
|
|
لاگ آپدیت لیست پارامترها.
|
|
"""
|
|
|
|
ACTION_ADDED = "added"
|
|
ACTION_MODIFIED = "modified"
|
|
ACTION_CHOICES = [
|
|
(ACTION_ADDED, "اضافه شده"),
|
|
(ACTION_MODIFIED, "ویرایش شده"),
|
|
]
|
|
|
|
parameter = models.ForeignKey(
|
|
SensorParameter,
|
|
on_delete=models.CASCADE,
|
|
related_name="update_logs",
|
|
)
|
|
action = models.CharField(max_length=16, choices=ACTION_CHOICES)
|
|
payload = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text="خلاصه تغییرات پارامتر برای audit",
|
|
)
|
|
updated_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
db_table = "farm_data_parameterupdatelog"
|
|
ordering = ["-updated_at"]
|
|
verbose_name = "لاگ آپدیت پارامتر"
|
|
verbose_name_plural = "لاگ آپدیت پارامترها"
|
|
|
|
def __str__(self):
|
|
return f"{self.parameter.code} - {self.action} - {self.updated_at}"
|