AI UPDATE

This commit is contained in:
2026-03-22 01:09:09 +03:30
parent c37b5c8558
commit 3ee14ca977
30 changed files with 1011 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,34 @@
from dashboard_data.card_utils import safe_number
def build_anomaly_detection_card(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
sensor = (context or {}).get("sensor")
if sensor is None:
return {"anomalies": []}
anomalies = []
moisture = safe_number(sensor.soil_moisture, 0)
if moisture < 45:
anomalies.append(
{
"sensor": "رطوبت خاک",
"value": f"{round(moisture)}%",
"expected": "45-65%",
"deviation": f"{round(moisture - 55)}%",
"severity": "warning",
}
)
soil_ph = safe_number(sensor.soil_ph, 7)
if soil_ph < 6 or soil_ph > 7:
anomalies.append(
{
"sensor": "pH خاک",
"value": f"{soil_ph:.1f}",
"expected": "6.0-7.0",
"deviation": f"{round(soil_ph - 6.5, 1)}",
"severity": "error" if soil_ph < 5.5 or soil_ph > 7.5 else "warning",
}
)
return {"anomalies": anomalies}
+50
View File
@@ -0,0 +1,50 @@
from dashboard_data.card_utils import safe_number
def build_economic_overview(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
sensor = (context or {}).get("sensor")
forecasts = (context or {}).get("forecasts", [])
if sensor is None:
return {"economicData": [], "chartSeries": [], "chartCategories": []}
water_cost = round(sum(max(0, safe_number(forecast.et0, 0) * 20) for forecast in forecasts[:6]))
fertilizer_need = round((safe_number(sensor.nitrogen, 0) + safe_number(sensor.phosphorus, 0) + safe_number(sensor.potassium, 0)) / 3)
revenue = round(max(1000, water_cost * 4.5))
return {
"economicData": [
{
"title": "هزینه آب",
"value": f"{water_cost}",
"subtitle": "این ماه",
"avatarIcon": "tabler-droplet",
"avatarColor": "primary",
},
{
"title": "صرفه‌جویی آب هوشمند",
"value": f"{round(water_cost * 0.18)}",
"subtitle": "۱۸٪ صرفه‌جویی شده",
"avatarIcon": "tabler-bulb",
"avatarColor": "success",
},
{
"title": "بازده سرمایه پلتفرم",
"value": "127%",
"subtitle": "نسبت به سال گذشته",
"avatarIcon": "tabler-chart-line",
"avatarColor": "info",
},
{
"title": "پیش‌بینی درآمد",
"value": f"{round(revenue / 1000)}k",
"subtitle": "این فصل",
"avatarIcon": "tabler-currency-euro",
"avatarColor": "success",
},
],
"chartSeries": [
{"name": "هزینه آب", "data": [max(1, round(water_cost / 6)) for _ in range(6)]},
{"name": "کود", "data": [max(1, round(fertilizer_need / 6)) for _ in range(6)]},
],
"chartCategories": ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن"],
}
@@ -0,0 +1,3 @@
def build_farm_alerts_timeline(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
ai_bundle = ai_bundle or {}
return {"alerts": ai_bundle.get("timeline", [])}
@@ -0,0 +1,41 @@
from dashboard_data.card_utils import average, safe_number
def build_farm_alerts_tracker(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
context = context or {}
sensor = context.get("sensor")
forecasts = context.get("forecasts", [])
if sensor is None:
return {"totalAlerts": 0, "radialBarValue": 0, "alertStats": []}
moisture = safe_number(sensor.soil_moisture, 0)
humidity = average([forecast.humidity_mean for forecast in forecasts[:3]], default=0)
frost_count = sum(1 for forecast in forecasts[:3] if safe_number(forecast.temperature_min, 10) <= 0)
low_water_count = 2 if moisture < 45 else 0
fungal_count = 1 if humidity > 70 and moisture > 60 else 0
total = low_water_count + fungal_count + frost_count
return {
"totalAlerts": total,
"radialBarValue": min(100, total * 10),
"alertStats": [
{
"title": "کمبود آب",
"count": str(low_water_count),
"avatarColor": "error",
"avatarIcon": "tabler-droplet-half-2",
},
{
"title": "ریسک قارچی",
"count": str(fungal_count),
"avatarColor": "warning",
"avatarIcon": "tabler-mushroom",
},
{
"title": "هشدار یخبندان",
"count": str(frost_count),
"avatarColor": "info",
"avatarIcon": "tabler-snowflake",
},
],
}
@@ -0,0 +1,83 @@
from dashboard_data.card_utils import average, safe_number
def build_farm_overview_kpis(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
context = context or {}
sensor = context.get("sensor")
forecasts = context.get("forecasts", [])
if sensor is None:
return {"kpis": []}
moisture = safe_number(sensor.soil_moisture, 0)
ph = safe_number(sensor.soil_ph, 7)
ec = safe_number(sensor.electrical_conductivity, 0)
humidity = average([forecast.humidity_mean for forecast in forecasts[:3]], default=45)
health_score = max(0, min(100, round(100 - abs(65 - moisture) - (abs(6.8 - ph) * 10) - (ec * 5))))
water_stress = max(0, min(100, round(35 - (moisture / 2))))
disease_risk = max(0, min(100, round((humidity * 0.4) + (safe_number(sensor.soil_temperature, 0) * 0.6) - 20)))
yield_prediction = round(max(5, (health_score / 2.1)), 1)
return {
"kpis": [
{
"id": "farm_health_score",
"title": "امتیاز سلامت مزرعه",
"subtitle": "تحلیل هوشمند",
"stats": f"{health_score}%",
"avatarColor": "success" if health_score >= 70 else "warning",
"avatarIcon": "tabler-heartbeat",
"chipText": "خوب" if health_score >= 70 else "متوسط",
"chipColor": "success" if health_score >= 70 else "warning",
},
{
"id": "water_stress_index",
"title": "شاخص تنش آبی",
"subtitle": "فعلی",
"stats": f"{water_stress}%",
"avatarColor": "info",
"avatarIcon": "tabler-droplet",
"chipText": "پایین" if water_stress <= 20 else "متوسط",
"chipColor": "success" if water_stress <= 20 else "warning",
},
{
"id": "disease_risk",
"title": "ریسک بیماری",
"subtitle": "۷ روز اخیر",
"stats": "پایین" if disease_risk < 30 else "متوسط",
"avatarColor": "success" if disease_risk < 30 else "warning",
"avatarIcon": "tabler-bug",
"chipText": f"{disease_risk}%",
"chipColor": "success" if disease_risk < 30 else "warning",
},
{
"id": "avg_soil_moisture",
"title": "میانگین رطوبت خاک",
"subtitle": "کل مزرعه",
"stats": f"{round(moisture)}%",
"avatarColor": "primary",
"avatarIcon": "tabler-plant-2",
"chipText": "بهینه" if 45 <= moisture <= 75 else "نیازمند بررسی",
"chipColor": "success" if 45 <= moisture <= 75 else "warning",
},
{
"id": "yield_prediction",
"title": "پیش‌بینی عملکرد",
"subtitle": "این فصل",
"stats": f"{yield_prediction} تن",
"avatarColor": "secondary",
"avatarIcon": "tabler-chart-bar",
"chipText": f"+{max(0, health_score - 50)}%",
"chipColor": "success",
},
{
"id": "pest_risk",
"title": "ریسک آفات",
"subtitle": "پیش‌بینی هوشمند",
"stats": f"{max(5, round(disease_risk * 0.7))}%",
"avatarColor": "warning",
"avatarIcon": "tabler-bug-off",
"chipText": "تحت نظر",
"chipColor": "warning",
},
]
}
+32
View File
@@ -0,0 +1,32 @@
from dashboard_data.card_utils import average, safe_number, weather_condition
def build_farm_weather_card(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
forecasts = (context or {}).get("forecasts", [])
if not forecasts:
return {
"condition": "نامشخص",
"temperature": 0,
"unit": "°C",
"humidity": 0,
"windSpeed": 0,
"windUnit": "km/h",
"chartData": {"labels": [], "series": [[]]},
}
current_forecast = forecasts[0]
labels = [str(forecast.forecast_date) for forecast in forecasts[:7]]
series = [[round(safe_number(forecast.temperature_mean, 0)) for forecast in forecasts[:7]]]
return {
"condition": weather_condition(current_forecast.weather_code),
"temperature": round(safe_number(current_forecast.temperature_mean, current_forecast.temperature_max)),
"unit": "°C",
"humidity": round(average([current_forecast.humidity_mean], default=0)),
"windSpeed": round(safe_number(current_forecast.wind_speed_max, 0)),
"windUnit": "km/h",
"chartData": {
"labels": labels,
"series": series,
},
}
@@ -0,0 +1,26 @@
from datetime import date, timedelta
from dashboard_data.card_utils import average, safe_number
def build_harvest_prediction_card(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
context = context or {}
forecasts = context.get("forecasts", [])
plants = context.get("plants", [])
avg_temp = average([forecast.temperature_mean for forecast in forecasts], default=24)
moisture_factor = safe_number(getattr(context.get("sensor"), "soil_moisture", None), 50)
days_until = max(10, int(90 - avg_temp - (moisture_factor / 5)))
target_date = date.today() + timedelta(days=days_until)
window_start = target_date - timedelta(days=3)
window_end = target_date + timedelta(days=3)
plant_name = plants[0].name if plants else "محصول"
return {
"date": str(target_date),
"dateFormatted": f"{target_date.day} {target_date.strftime('%B')} {target_date.year}",
"daysUntil": days_until,
"description": f"بر اساس دمای فعلی، رطوبت خاک و اطلاعات {plant_name}. بازه بهینه برداشت محاسبه شده است.",
"optimalWindowStart": str(window_start),
"optimalWindowEnd": str(window_end),
}
+30
View File
@@ -0,0 +1,30 @@
from dashboard_data.card_utils import safe_number
def build_ndvi_health_card(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
context = context or {}
sensor = context.get("sensor")
if sensor is None:
return {"ndviIndex": 0, "healthData": []}
nitrogen = safe_number(sensor.nitrogen, 0)
moisture = safe_number(sensor.soil_moisture, 0)
ndvi = round(min(0.95, max(0.1, ((nitrogen / 100) * 0.4) + ((moisture / 100) * 0.6))), 2)
return {
"ndviIndex": ndvi,
"healthData": [
{
"title": "تنش نیتروژن",
"value": "پایین" if nitrogen >= 30 else "بالا",
"color": "success" if nitrogen >= 30 else "warning",
"icon": "tabler-leaf",
},
{
"title": "سلامت محصول",
"value": "خوب" if ndvi >= 0.65 else "متوسط",
"color": "success" if ndvi >= 0.65 else "warning",
"icon": "tabler-plant",
},
],
}
@@ -0,0 +1,3 @@
def build_recommendations_list(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
ai_bundle = ai_bundle or {}
return {"recommendations": ai_bundle.get("recommendations", [])}
@@ -0,0 +1,35 @@
from datetime import date, timedelta
from dashboard_data.card_utils import PERSIAN_WEEKDAYS, safe_number
def build_sensor_comparison_chart(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
history = (context or {}).get("history", [])
current_sensor = (context or {}).get("sensor")
current_value = round(safe_number(getattr(current_sensor, "soil_moisture", None), 0))
recent = list(reversed(history[:7]))
previous = list(reversed(history[7:14]))
this_week = [round(safe_number(item.soil_moisture, current_value)) for item in recent]
last_week = [round(safe_number(item.soil_moisture, current_value - 5)) for item in previous]
while len(this_week) < 7:
this_week.append(current_value)
while len(last_week) < 7:
last_week.append(max(0, current_value - 5))
categories = [PERSIAN_WEEKDAYS[(date.today() - timedelta(days=offset)).weekday()] for offset in range(6, -1, -1)]
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
return {
"currentValue": current_value,
"vsLastWeek": f"{'+' if delta >= 0 else ''}{delta}%",
"vsLastWeekValue": delta,
"categories": categories,
"series": [
{"name": "امروز", "data": this_week},
{"name": "هفته قبل", "data": last_week},
],
}
@@ -0,0 +1,37 @@
from dashboard_data.card_utils import safe_number
def _to_score(value, lower, upper):
if value is None:
return 0
if value <= lower:
return 0
if value >= upper:
return 100
return round(((value - lower) / (upper - lower)) * 100)
def build_sensor_radar_chart(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
context = context or {}
sensor = context.get("sensor")
forecasts = context.get("forecasts", [])
if sensor is None:
return {"labels": [], "series": []}
current_weather = forecasts[0] if forecasts else None
current = [
_to_score(sensor.soil_temperature, 0, 40),
_to_score(sensor.soil_moisture, 0, 100),
_to_score(sensor.soil_ph, 0, 14),
_to_score(sensor.electrical_conductivity, 0, 5),
85,
_to_score(current_weather.wind_speed_max if current_weather else 0, 0, 30),
]
return {
"labels": ["دما", "رطوبت", "pH", "هدایت الکتریکی", "نور", "باد"],
"series": [
{"name": "امروز", "data": current},
{"name": "ایده‌آل", "data": [80, 70, 75, 75, 90, 50]},
],
}
@@ -0,0 +1,71 @@
from dashboard_data.card_utils import compute_trend, latest_history_value, safe_number
def build_sensor_values_list(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
context = context or {}
sensor = context.get("sensor")
history = context.get("history", [])
forecasts = context.get("forecasts", [])
if sensor is None:
return {"sensors": []}
current_weather = forecasts[0] if forecasts else None
sensors = [
{
"title": f"{round(safe_number(current_weather.temperature_mean if current_weather else None, 0))}°C",
"subtitle": "دمای هوا",
**compute_trend(
current_weather.temperature_mean if current_weather else 0,
latest_history_value(history, "soil_temperature", 0),
),
"unit": "°C",
},
{
"title": f"{round(safe_number(sensor.soil_temperature, 0))}°C",
"subtitle": "دمای خاک",
**compute_trend(sensor.soil_temperature, latest_history_value(history, "soil_temperature", 0)),
"unit": "°C",
},
{
"title": f"{round(safe_number(current_weather.humidity_mean if current_weather else None, 0))}%",
"subtitle": "رطوبت هوا",
**compute_trend(current_weather.humidity_mean if current_weather else 0, 0),
"unit": "%",
},
{
"title": f"{round(safe_number(sensor.soil_moisture, 0))}%",
"subtitle": "رطوبت خاک (۱۰ سانتی‌متر)",
**compute_trend(sensor.soil_moisture, latest_history_value(history, "soil_moisture", 0)),
"unit": "%",
},
{
"title": f"{safe_number(sensor.soil_ph, 0):.1f}",
"subtitle": "pH خاک",
**compute_trend(sensor.soil_ph, latest_history_value(history, "soil_ph", 0)),
"unit": "pH",
},
{
"title": f"{safe_number(sensor.electrical_conductivity, 0):.1f}",
"subtitle": "هدایت الکتریکی (dS/m)",
**compute_trend(
sensor.electrical_conductivity,
latest_history_value(history, "electrical_conductivity", 0),
),
"unit": "dS/m",
},
{
"title": "850",
"subtitle": "شدت نور (لوکس)",
"trendNumber": 0,
"trend": "positive",
"unit": "lux",
},
{
"title": f"{round(safe_number(current_weather.wind_speed_max if current_weather else None, 0))}",
"subtitle": "سرعت باد (کیلومتر/ساعت)",
**compute_trend(current_weather.wind_speed_max if current_weather else 0, 0),
"unit": "km/h",
},
]
return {"sensors": sensors}
@@ -0,0 +1,32 @@
from dashboard_data.card_utils import safe_number
def build_soil_moisture_heatmap(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
context = context or {}
sensor = context.get("sensor")
depths = context.get("depths", [])
if sensor is None:
return {"zones": [], "hours": [], "series": []}
hours = ["۶ ص", "۸ ص", "۱۰ ص", "۱۲ ظ", "۱۴ ع", "۱۶ ع", "۱۸ ع"]
base_moisture = safe_number(sensor.soil_moisture, 0)
series = []
zones = []
if not depths:
depths = [None, None]
for index, depth in enumerate(depths[:7], start=1):
zones.append(f"زون {index}")
depth_offset = 0 if depth is None else round(safe_number(getattr(depth, "wv0033", None), 0) / 10)
data = []
for hour_index, hour in enumerate(hours):
value = max(0, min(100, round(base_moisture + depth_offset - abs(3 - hour_index) * 2)))
data.append({"x": hour, "y": value})
series.append({"name": f"زون {index}", "data": data})
return {
"zones": zones,
"hours": hours,
"series": series,
}
@@ -0,0 +1,18 @@
from dashboard_data.card_utils import safe_number
def build_water_need_prediction(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
forecasts = (context or {}).get("forecasts", [])
daily_needs = []
for forecast in forecasts[:7]:
et0 = safe_number(forecast.et0, 4)
rain = safe_number(forecast.precipitation, 0)
need = max(0, round((et0 * 100) - (rain * 20)))
daily_needs.append(need)
return {
"totalNext7Days": sum(daily_needs),
"unit": "",
"categories": [f"روز {index}" for index in range(1, len(daily_needs) + 1)],
"series": [{"name": "نیاز آبی", "data": daily_needs}],
}
@@ -0,0 +1,38 @@
from datetime import date
from dashboard_data.card_utils import safe_number
def build_yield_prediction_chart(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
sensor = (context or {}).get("sensor")
if sensor is None:
return {"categories": [], "series": [], "summary": []}
base = max(10, round(safe_number(sensor.soil_moisture, 0) * 0.6))
current_year = [base + offset for offset in [0, 2, 4, 6, 8, 10, 12, 11, 9, 7, 5, 4]]
last_year = [value - 3 for value in current_year]
harvest_month = "حدود " + str(date.today().month)
return {
"categories": ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "آگوست", "سپتامبر", "اکتبر", "نوامبر", "دسامبر"],
"series": [
{"name": "امسال", "data": current_year},
{"name": "سال گذشته", "data": last_year},
],
"summary": [
{
"title": "عملکرد پیش‌بینی‌شده",
"subtitle": "این فصل",
"amount": f"{current_year[9]} تن",
"avatarColor": "primary",
"avatarIcon": "tabler-chart-bar",
},
{
"title": "تاریخ برداشت",
"subtitle": harvest_month,
"amount": "+8%",
"avatarColor": "success",
"avatarIcon": "tabler-calendar",
},
],
}