Files
Ai/dashboard_data/cards/CARD_FORMULAS.md
T

873 lines
19 KiB
Markdown
Raw Normal View History

2026-03-22 03:08:27 +03:30
# مستند فرمول‌های `dashboard_data/cards`
این فایل توضیح می‌دهد هر کارت در `dashboard_data/cards` چطور داده‌های خروجی خود را محاسبه می‌کند، از چه فیلدهایی استفاده می‌کند، و چه fallbackهایی دارد.
## منبع داده‌های مشترک
کانتکست بیشتر کارت‌ها از `dashboard_data/context.py` می‌آید:
- `sensor`: رکورد اصلی سنسور
- `location`: لوکیشن سنسور
- `depths`: داده‌های عمق خاک از `SoilDepthData`
- `forecasts`: حداکثر ۷ پیش‌بینی آب‌وهوا از امروز به بعد
- `history`: حداکثر ۳۰ رکورد تاریخچه سنسور، مرتب‌شده از جدید به قدیم
- `plants`: گیاه‌های متصل به سنسور
- `irrigation_methods`: حداکثر ۵ روش آبیاری
## توابع کمکی مشترک
این توابع در `dashboard_data/card_utils.py` استفاده می‌شوند:
- `safe_number(value, default=0)`: اگر مقدار `None` باشد، `default` برمی‌گرداند.
- `average(values, default=0)`: میانگین مقادیر غیر `None` را می‌دهد؛ اگر هیچ مقداری نبود `default` برمی‌گرداند.
- `latest_history_value(history, field_name, default=None)`: مقدار `field_name` را از جدیدترین رکورد history می‌گیرد.
- `compute_trend(current, previous)`:
- `diff = round(current_value - previous_value, 1)`
- `trend = "positive"` اگر `diff >= 0`، وگرنه `"negative"`
---
## 1) `farm_overview_kpis.py`
تابع سازنده: `build_farm_overview_kpis`
### ورودی‌های اصلی
- `sensor.soil_moisture``moisture`
- `sensor.soil_ph``ph`
- `sensor.electrical_conductivity``ec`
- `sensor.soil_temperature`
- میانگین `forecast.humidity_mean` برای ۳ پیش‌بینی اول → `humidity`
### فرمول‌ها
#### 1. امتیاز سلامت مزرعه (`farm_health_score`)
```text
health_score = clamp(
round(
100
- abs(65 - moisture)
- (abs(6.8 - ph) * 10)
- (ec * 5)
),
0,
100
)
```
توضیح:
- رطوبت ایده‌آل ۶۵٪ فرض شده.
- pH ایده‌آل ۶.۸ فرض شده.
- EC بالاتر، امتیاز را کم می‌کند.
آستانه‌های نمایش:
- اگر `health_score >= 70` → وضعیت `خوب` و رنگ `success`
- در غیر این صورت → `متوسط` و رنگ `warning`
#### 2. شاخص تنش آبی (`water_stress_index`)
```text
water_stress = clamp(round(35 - (moisture / 2)), 0, 100)
```
توضیح:
- هرچه رطوبت خاک بیشتر شود، تنش آبی کمتر می‌شود.
آستانه نمایش:
- اگر `water_stress <= 20``پایین`
- در غیر این صورت → `متوسط`
#### 3. ریسک بیماری (`disease_risk`)
```text
disease_risk = clamp(
round((humidity * 0.4) + (soil_temperature * 0.6) - 20),
0,
100
)
```
توضیح:
- دمای خاک وزن ۶۰٪ دارد.
- رطوبت هوا وزن ۴۰٪ دارد.
آستانه نمایش:
- اگر `disease_risk < 30``پایین`
- در غیر این صورت → `متوسط`
#### 4. میانگین رطوبت خاک (`avg_soil_moisture`)
```text
avg_soil_moisture = round(moisture)
```
نکته:
- اینجا عملاً فقط از `sensor.soil_moisture` فعلی استفاده می‌شود و واقعاً میانگین چند سنسور یا چند ناحیه محاسبه نمی‌شود.
آستانه نمایش:
- اگر `45 <= moisture <= 75``بهینه`
- در غیر این صورت → `نیازمند بررسی`
#### 5. پیش‌بینی عملکرد (`yield_prediction`)
```text
yield_prediction = round(max(5, health_score / 2.1), 1)
```
فرمول متن chip:
```text
yield_chip = "+" + str(max(0, health_score - 50)) + "%"
```
#### 6. ریسک آفات (`pest_risk`)
```text
pest_risk = max(5, round(disease_risk * 0.7))
```
نکته:
- ریسک آفات به‌صورت مستقیم از ۷۰٪ ریسک بیماری ساخته شده.
---
## 2) `farm_weather_card.py`
تابع سازنده: `build_farm_weather_card`
### منطق کلی
اگر `forecasts` خالی باشد:
- `condition = "نامشخص"`
- `temperature = 0`
- `humidity = 0`
- `windSpeed = 0`
- `chartData.labels = []`
- `chartData.series = [[]]`
در غیر این صورت:
### فرمول‌ها
#### 1. وضعیت آب‌وهوا
```text
condition = weather_condition(current_forecast.weather_code)
```
که `weather_code` با جدول `WMO_CONDITIONS` به متن فارسی تبدیل می‌شود.
#### 2. دما
```text
temperature = round(
safe_number(current_forecast.temperature_mean, current_forecast.temperature_max)
)
```
یعنی:
- اول `temperature_mean`
- اگر `None` بود، `temperature_max`
#### 3. رطوبت
```text
humidity = round(average([current_forecast.humidity_mean], default=0))
```
نکته:
- چون فقط یک مقدار داخل `average` قرار می‌گیرد، عملاً همان `humidity_mean` فعلی است.
#### 4. سرعت باد
```text
windSpeed = round(safe_number(current_forecast.wind_speed_max, 0))
```
#### 5. نمودار دما
برای ۷ روز اول:
```text
labels = [str(forecast.forecast_date) for forecast in forecasts[:7]]
series = [[round(safe_number(forecast.temperature_mean, 0)) for forecast in forecasts[:7]]]
```
---
## 3) `farm_alerts_tracker.py`
تابع سازنده: `build_farm_alerts_tracker`
### ورودی‌ها
- `sensor.soil_moisture``moisture`
- میانگین `humidity_mean` برای ۳ forecast اول → `humidity`
- `temperature_min` برای ۳ forecast اول
### فرمول‌ها
#### 1. هشدار کمبود آب
```text
low_water_count = 2 if moisture < 45 else 0
```
#### 2. هشدار ریسک قارچی
```text
fungal_count = 1 if (humidity > 70 and moisture > 60) else 0
```
#### 3. هشدار یخبندان
```text
frost_count = count(
forecast for first 3 forecasts
if temperature_min <= 0
)
```
در کد:
```text
frost_count = sum(
1 for forecast in forecasts[:3]
if safe_number(forecast.temperature_min, 10) <= 0
)
```
#### 4. مجموع هشدارها
```text
totalAlerts = low_water_count + fungal_count + frost_count
```
#### 5. مقدار radial bar
```text
radialBarValue = min(100, totalAlerts * 10)
```
---
## 4) `sensor_values_list.py`
تابع سازنده: `build_sensor_values_list`
این کارت برای هر آیتم، مقدار فعلی و trend را می‌سازد.
### فرمول trend
برای هر سنسور که از `compute_trend` استفاده می‌کند:
```text
trendNumber = round(current - previous, 1)
trend = "positive" if trendNumber >= 0 else "negative"
```
### آیتم‌ها
#### 1. دمای هوا
```text
title = round(current_weather.temperature_mean or 0) + "°C"
previous = latest_history_value(history, "soil_temperature", 0)
```
نکته مهم:
- trend دمای هوا با `soil_temperature` از history مقایسه می‌شود، نه با history دمای هوا.
#### 2. دمای خاک
```text
current = sensor.soil_temperature
previous = latest_history_value(history, "soil_temperature", 0)
```
#### 3. رطوبت هوا
```text
current = current_weather.humidity_mean or 0
previous = 0
```
نکته:
- همیشه نسبت به صفر trend می‌گیرد، نه history.
#### 4. رطوبت خاک
```text
current = sensor.soil_moisture
previous = latest_history_value(history, "soil_moisture", 0)
```
#### 5. pH خاک
```text
current = sensor.soil_ph
previous = latest_history_value(history, "soil_ph", 0)
```
#### 6. هدایت الکتریکی
```text
current = sensor.electrical_conductivity
previous = latest_history_value(history, "electrical_conductivity", 0)
```
#### 7. شدت نور
```text
title = "850"
trendNumber = 0
trend = "positive"
```
نکته:
- این مقدار کاملاً ثابت (hard-coded) است و فرمولی ندارد.
#### 8. سرعت باد
```text
current = current_weather.wind_speed_max or 0
previous = 0
```
نکته:
- trend سرعت باد هم نسبت به صفر محاسبه می‌شود.
---
## 5) `sensor_radar_chart.py`
تابع سازنده: `build_sensor_radar_chart`
### تابع نرمال‌سازی
```text
to_score(value, lower, upper):
if value is None -> 0
if value <= lower -> 0
if value >= upper -> 100
else -> round(((value - lower) / (upper - lower)) * 100)
```
### سری «امروز»
به‌ترتیب:
```text
soil_temperature_score = to_score(sensor.soil_temperature, 0, 40)
soil_moisture_score = to_score(sensor.soil_moisture, 0, 100)
soil_ph_score = to_score(sensor.soil_ph, 0, 14)
ec_score = to_score(sensor.electrical_conductivity, 0, 5)
light_score = 85
wind_score = to_score(current_weather.wind_speed_max if current_weather else 0, 0, 30)
```
خروجی:
```text
series[0].data = [
soil_temperature_score,
soil_moisture_score,
soil_ph_score,
ec_score,
85,
wind_score
]
```
### سری «ایده‌آل»
```text
[80, 70, 75, 75, 90, 50]
```
نکته:
- این مقادیر ثابت هستند و از دیتابیس محاسبه نمی‌شوند.
---
## 6) `sensor_comparison_chart.py`
تابع سازنده: `build_sensor_comparison_chart`
### ورودی‌ها
- `current_sensor.soil_moisture``current_value`
- `history[:7]` → داده‌های هفته جاری
- `history[7:14]` → داده‌های هفته قبل
### فرمول‌ها
#### 1. مقدار فعلی
```text
currentValue = round(sensor.soil_moisture)
```
#### 2. سری هفته جاری
```text
recent = reversed(history[:7])
this_week = [round(item.soil_moisture or current_value) for item in recent]
```
اگر کمتر از ۷ مقدار باشد:
```text
while len(this_week) < 7:
this_week.append(current_value)
```
#### 3. سری هفته قبل
```text
previous = reversed(history[7:14])
last_week = [round(item.soil_moisture or (current_value - 5)) for item in previous]
```
اگر کمتر از ۷ مقدار باشد:
```text
while len(last_week) < 7:
last_week.append(max(0, current_value - 5))
```
#### 4. درصد تغییر نسبت به هفته قبل
```text
avg_this = sum(this_week) / len(this_week)
avg_last = sum(last_week) / len(last_week)
delta = round(((avg_this - avg_last) / avg_last) * 100) if avg_last else 0
```
نمایش متن:
```text
vsLastWeek = f"{'+' if delta >= 0 else ''}{delta}%"
vsLastWeekValue = delta
```
#### 5. دسته‌بندی روزها
روزهای ۷ روز اخیر با نام فارسی weekday ساخته می‌شوند:
```text
categories = [
PERSIAN_WEEKDAYS[(today - offset_days).weekday()]
for offset_days in range(6, -1, -1)
]
```
---
## 7) `anomaly_detection_card.py`
تابع سازنده: `build_anomaly_detection_card`
این کارت anomalyها را فقط برای دو شاخص تولید می‌کند: رطوبت خاک و pH خاک.
### 1. anomaly رطوبت خاک
فقط وقتی ساخته می‌شود که:
```text
moisture < 45
```
ساختار خروجی:
```text
value = round(moisture) + "%"
expected = "45-65%"
deviation = round(moisture - 55) + "%"
severity = "warning"
```
نکته:
- deviation نسبت به نقطه مرجع ۵۵٪ محاسبه می‌شود، نه مرز ۴۵٪.
### 2. anomaly pH خاک
فقط وقتی ساخته می‌شود که:
```text
soil_ph < 6 or soil_ph > 7
```
ساختار خروجی:
```text
value = format(soil_ph, ".1f")
expected = "6.0-7.0"
deviation = round(soil_ph - 6.5, 1)
severity = "error" if (soil_ph < 5.5 or soil_ph > 7.5) else "warning"
```
---
## 8) `farm_alerts_timeline.py`
تابع سازنده: `build_farm_alerts_timeline`
### منطق
این کارت هیچ فرمول داخلی ندارد و داده را مستقیماً از `ai_bundle` برمی‌دارد:
```text
alerts = ai_bundle.get("timeline", [])
```
---
## 9) `water_need_prediction.py`
تابع سازنده: `build_water_need_prediction`
برای ۷ forecast اول:
### فرمول نیاز آبی روزانه
```text
et0 = safe_number(forecast.et0, 4)
rain = safe_number(forecast.precipitation, 0)
need = max(0, round((et0 * 100) - (rain * 20)))
```
توضیح:
- `ET0` در ۱۰۰ ضرب می‌شود.
- بارش در ۲۰ ضرب و از آن کم می‌شود.
- مقدار منفی به صفر clamp می‌شود.
### خروجی نهایی
```text
daily_needs = [need for first 7 forecasts]
totalNext7Days = sum(daily_needs)
categories = ["روز 1", "روز 2", ...]
series = [{"name": "نیاز آبی", "data": daily_needs}]
```
واحد خروجی:
```text
unit = "m³"
```
---
## 10) `harvest_prediction_card.py`
تابع سازنده: `build_harvest_prediction_card`
### ورودی‌ها
- میانگین `temperature_mean` تمام forecastها → `avg_temp`
- `sensor.soil_moisture``moisture_factor`
- نام اولین گیاه → `plant_name`
### فرمول‌ها
#### 1. میانگین دما
```text
avg_temp = average([forecast.temperature_mean for forecast in forecasts], default=24)
```
#### 2. فاکتور رطوبت
```text
moisture_factor = sensor.soil_moisture if available else 50
```
#### 3. روز باقی‌مانده تا برداشت
```text
days_until = max(10, int(90 - avg_temp - (moisture_factor / 5)))
```
توضیح:
- هرچه دمای متوسط بیشتر باشد، `days_until` کمتر می‌شود.
- هرچه رطوبت خاک بیشتر باشد، `days_until` کمتر می‌شود.
- حداقل ۱۰ روز است.
#### 4. تاریخ برداشت و بازه بهینه
```text
target_date = today + days_until
optimalWindowStart = target_date - 3 days
optimalWindowEnd = target_date + 3 days
```
#### 5. توضیح متنی
اگر گیاه وجود داشته باشد:
```text
description = "بر اساس دمای فعلی، رطوبت خاک و اطلاعات <plant_name>. بازه بهینه برداشت محاسبه شده است."
```
اگر گیاهی وجود نداشته باشد:
```text
plant_name = "محصول"
```
---
## 11) `yield_prediction_chart.py`
تابع سازنده: `build_yield_prediction_chart`
### فرمول‌ها
#### 1. مقدار پایه
```text
base = max(10, round(sensor.soil_moisture * 0.6))
```
#### 2. سری سال جاری
```text
current_year = [
base + 0,
base + 2,
base + 4,
base + 6,
base + 8,
base + 10,
base + 12,
base + 11,
base + 9,
base + 7,
base + 5,
base + 4
]
```
#### 3. سری سال گذشته
```text
last_year = [value - 3 for value in current_year]
```
#### 4. خلاصه کارت
عملکرد پیش‌بینی‌شده:
```text
summary[0].amount = current_year[9] + " تن"
```
یعنی مقدار ماه دهم لیست (اندیس ۹).
تاریخ برداشت:
```text
harvest_month = "حدود " + str(today.month)
summary[1].amount = "+8%"
```
نکته:
- `+8%` مقدار ثابت است و از فرمول نیامده.
---
## 12) `soil_moisture_heatmap.py`
تابع سازنده: `build_soil_moisture_heatmap`
### ورودی‌ها
- `sensor.soil_moisture``base_moisture`
- `depth.wv0033` برای هر لایه عمق خاک
### منطق اولیه
```text
hours = ["۶ ص", "۸ ص", "۱۰ ص", "۱۲ ظ", "۱۴ ع", "۱۶ ع", "۱۸ ع"]
```
اگر `depths` خالی باشد:
```text
depths = [None, None]
```
### فرمول zone offset
برای هر depth:
```text
depth_offset = 0 if depth is None else round(depth.wv0033 / 10)
```
### فرمول هر خانه heatmap
برای هر zone و هر ساعت:
```text
value = clamp(
round(base_moisture + depth_offset - abs(3 - hour_index) * 2),
0,
100
)
```
توضیح:
- `hour_index = 3` مرکز نمودار است و بیشترین مقدار را می‌دهد.
- هرچه از مرکز دورتر شویم، به ازای هر پله ۲ واحد کم می‌شود.
ساختار خروجی هر نقطه:
```text
{"x": hour, "y": value}
```
---
## 13) `ndvi_health_card.py`
تابع سازنده: `build_ndvi_health_card`
### ورودی‌ها
- `sensor.nitrogen``nitrogen`
- `sensor.soil_moisture``moisture`
### فرمول NDVI
```text
ndvi = round(
clamp(((nitrogen / 100) * 0.4) + ((moisture / 100) * 0.6), 0.1, 0.95),
2
)
```
توضیح:
- نیتروژن ۴۰٪ وزن دارد.
- رطوبت خاک ۶۰٪ وزن دارد.
- خروجی بین `0.1` و `0.95` محدود می‌شود.
### وضعیت‌های متنی
#### 1. تنش نیتروژن
```text
"پایین" if nitrogen >= 30 else "بالا"
```
#### 2. سلامت محصول
```text
"خوب" if ndvi >= 0.65 else "متوسط"
```
---
## 14) `recommendations_list.py`
تابع سازنده: `build_recommendations_list`
### منطق
این کارت فرمول داخلی ندارد:
```text
recommendations = ai_bundle.get("recommendations", [])
```
---
## 15) `economic_overview.py`
تابع سازنده: `build_economic_overview`
### ورودی‌ها
- `forecast.et0` برای ۶ forecast اول
- `sensor.nitrogen`
- `sensor.phosphorus`
- `sensor.potassium`
### فرمول‌ها
#### 1. هزینه آب
```text
water_cost = round(sum(max(0, forecast.et0 * 20) for first 6 forecasts))
```
در کد با fallback:
```text
water_cost = round(
sum(max(0, safe_number(forecast.et0, 0) * 20) for forecast in forecasts[:6])
)
```
#### 2. نیاز کودی
```text
fertilizer_need = round((nitrogen + phosphorus + potassium) / 3)
```
با fallback صفر برای هر کدام.
#### 3. پیش‌بینی درآمد
```text
revenue = round(max(1000, water_cost * 4.5))
```
#### 4. صرفه‌جویی آب هوشمند
```text
smart_saving = round(water_cost * 0.18)
```
### chartSeries
#### سری هزینه آب
```text
water_series = [max(1, round(water_cost / 6)) for _ in range(6)]
```
#### سری کود
```text
fertilizer_series = [max(1, round(fertilizer_need / 6)) for _ in range(6)]
```
نکته:
- هر دو سری، ۶ مقدار تکراری یکسان تولید می‌کنند و روند ماهانه واقعی ندارند.
---
## کارت‌های بدون فرمول محاسباتی
این کارت‌ها فقط داده را از `ai_bundle` می‌خوانند:
- `farm_alerts_timeline.py`
- `recommendations_list.py`
---
## نکات مهم برای تیم
- چند اسم کارت با واقعیت محاسبه‌شان دقیقاً منطبق نیست؛ مثلاً `avg_soil_moisture` واقعاً average چند منبع نیست.
- بعضی trendها نسبت به history درستِ همان شاخص محاسبه نمی‌شوند؛ مخصوصاً در `sensor_values_list.py`.
- چند مقدار hard-coded هستند، مثل:
- `light_score = 85`
- `sensor_values_list` برای نور = `850`
- `yield_prediction_chart` برای برداشت = `+8%`
- سری ایده‌آل در `sensor_radar_chart`
- چند کارت به‌جای مدل تحلیلی واقعی، از فرمول‌های heuristic ساده استفاده می‌کنند.