This commit is contained in:
2026-04-25 17:22:41 +03:30
parent 569d520a5c
commit aa24fc22b0
124 changed files with 8491 additions and 2582 deletions
+147
View File
@@ -0,0 +1,147 @@
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 "نیازمند بررسی",
}