from __future__ import annotations from typing import Any DEFAULT_HEALTH_PROFILE = { "moisture": {"ideal_value": 65.0, "min_range": 45.0, "max_range": 75.0, "weight": 0.45}, "ph": {"ideal_value": 6.6, "min_range": 6.0, "max_range": 7.5, "weight": 0.30}, "ec": {"ideal_value": 1.2, "min_range": 0.2, "max_range": 3.0, "weight": 0.25}, } METRIC_SPECS = { "moisture": {"sensor_field": "soil_moisture", "label": "رطوبت خاک", "unit": "%"}, "ph": {"sensor_field": "soil_ph", "label": "pH خاک", "unit": "pH"}, "ec": {"sensor_field": "electrical_conductivity", "label": "هدایت الکتریکی", "unit": "dS/m"}, } def _safe_number(value: Any, default: float = 0.0) -> float: return default if value is None else float(value) def _normalize_metric(value: float, ideal_value: float, min_range: float, max_range: float) -> float: if max_range <= min_range: return 0.0 if value <= min_range or value >= max_range: return 0.0 if value == ideal_value: return 1.0 if value < ideal_value: span = ideal_value - min_range if span <= 0: return 0.0 return max(0.0, min(1.0, (value - min_range) / span)) span = max_range - ideal_value if span <= 0: return 0.0 return max(0.0, min(1.0, (max_range - value) / span)) def resolve_plant_profile(plants: list[Any]) -> tuple[dict[str, dict[str, float]], str]: for plant in plants: profile = getattr(plant, "health_profile", None) or {} if profile: merged = { metric: { **DEFAULT_HEALTH_PROFILE.get(metric, {}), **profile.get(metric, {}), } for metric in set(DEFAULT_HEALTH_PROFILE) | set(profile) } return merged, getattr(plant, "name", "گیاه") return DEFAULT_HEALTH_PROFILE, (plants[0].name if plants else "پروفایل پیش‌فرض") def compute_health_score(sensor: Any, profile: dict[str, dict[str, float]]) -> tuple[int, list[dict[str, Any]]]: weighted_sum = 0.0 total_weight = 0.0 components: list[dict[str, Any]] = [] for metric_type, config in profile.items(): spec = METRIC_SPECS.get(metric_type) if spec is None: continue sensor_value = getattr(sensor, spec["sensor_field"], None) if sensor_value is None: continue current_value = _safe_number(sensor_value, 0) defaults = DEFAULT_HEALTH_PROFILE.get(metric_type, {}) ideal_value = float(config.get("ideal_value", defaults.get("ideal_value", 0))) min_range = float(config.get("min_range", defaults.get("min_range", 0))) max_range = float(config.get("max_range", defaults.get("max_range", 0))) weight = float(config.get("weight", defaults.get("weight", 0))) if weight <= 0: continue normalized_value = _normalize_metric(current_value, ideal_value, min_range, max_range) weighted_sum += weight * normalized_value total_weight += weight components.append( { "metricType": metric_type, "label": spec["label"], "unit": spec["unit"], "currentValue": round(current_value, 2), "idealValue": round(ideal_value, 2), "minRange": round(min_range, 2), "maxRange": round(max_range, 2), "weight": round(weight, 3), "normalizedValue": round(normalized_value, 4), "weightedContribution": round(weight * normalized_value, 4), } ) if total_weight <= 0: return 0, components score = round((weighted_sum / total_weight) * 100) return max(0, min(100, score)), components def health_language(health_score: int) -> dict[str, str]: if health_score >= 85: return { "short_chip_text": "بسیار خوب", "action_hint": "برنامه فعلی پایش و نگهداری حفظ شود.", "explanation": "بیشتر شاخص های کلیدی نزدیک به پروفایل ایده آل گیاه هستند.", } if health_score >= 70: return { "short_chip_text": "پایدار", "action_hint": "تنظیمات فعلی حفظ و فقط شاخص های مرزی پایش شوند.", "explanation": "وضعیت کلی مزرعه قابل قبول است اما بعضی شاخص ها هنوز جای بهبود دارند.", } if health_score >= 50: return { "short_chip_text": "نیازمند تنظیم", "action_hint": "پارامترهای دور از محدوده ایده آل در اولویت اصلاح قرار گیرند.", "explanation": "بخشی از شرایط محیطی از پروفایل مطلوب گیاه فاصله گرفته است.", } return { "short_chip_text": "تنش بالا", "action_hint": "اصلاح فوری رطوبت، تغذیه يا شوری بر اساس اجزای امتیاز انجام شود.", "explanation": "چند شاخص اصلی خارج از بازه قابل قبول گیاه هستند.", } def build_soil_health_summary(sensor: Any, plants: list[Any]) -> dict[str, Any]: profile, profile_source = resolve_plant_profile(plants) health_score, health_components = compute_health_score(sensor, profile) moisture = _safe_number(getattr(sensor, "soil_moisture", None), 0) language = health_language(health_score) return { "healthScore": health_score, "profileSource": profile_source, "healthScoreDetails": { "method": "normalized_weighted_average", "profileSource": profile_source, "components": health_components, }, "healthLanguage": language, "avgSoilMoisture": round(moisture), "avgSoilMoistureRaw": round(moisture, 2), "avgSoilMoistureStatus": "بهینه" if 45 <= moisture <= 75 else "نیازمند بررسی", }