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}"