Files
Ai/dashboard_data/cards/sensor_comparison_chart.py
T

126 lines
4.6 KiB
Python
Raw Normal View History

2026-03-22 03:08:27 +03:30
from __future__ import annotations
2026-03-22 01:09:09 +03:30
from datetime import date, timedelta
2026-03-22 03:08:27 +03:30
from typing import Any
from dashboard_data.card_utils import PERSIAN_WEEKDAYS
INTERPOLATION_LIMIT = 3
QUALITY_REAL = "REAL"
QUALITY_INTERPOLATED = "INTERPOLATED"
QUALITY_MISSING = "MISSING"
def _day_categories() -> list[date]:
return [date.today() - timedelta(days=offset) for offset in range(6, -1, -1)]
def _day_label(day: date) -> str:
return PERSIAN_WEEKDAYS[day.weekday()]
def _history_value_map(history: list[Any], field_name: str) -> dict[date, float | None]:
value_map: dict[date, float | None] = {}
for item in history:
timestamp = getattr(item, "recorded_at", None)
if timestamp is None:
continue
day = timestamp.date()
if day in value_map:
continue
value = getattr(item, field_name, None)
value_map[day] = float(value) if value is not None else None
return value_map
def _apply_linear_interpolation(points: list[dict[str, Any]], limit: int = INTERPOLATION_LIMIT) -> list[dict[str, Any]]:
output = [dict(point) for point in points]
known_indexes = [index for index, point in enumerate(output) if point["value"] is not None]
for start_index, end_index in zip(known_indexes, known_indexes[1:]):
gap = end_index - start_index - 1
if gap <= 0 or gap > limit:
continue
start_value = output[start_index]["value"]
end_value = output[end_index]["value"]
if start_value is None or end_value is None:
continue
step = (end_value - start_value) / (gap + 1)
for offset in range(1, gap + 1):
target_index = start_index + offset
output[target_index]["value"] = round(start_value + (step * offset), 2)
output[target_index]["quality_flag"] = QUALITY_INTERPOLATED
return output
def _build_week_points(
history: list[Any],
field_name: str,
day_offset_start: int,
) -> list[dict[str, Any]]:
days = [date.today() - timedelta(days=offset) for offset in range(day_offset_start + 6, day_offset_start - 1, -1)]
value_map = _history_value_map(history, field_name)
raw_points = [
{
"timestamp": day.isoformat(),
"value": round(value_map[day], 2) if day in value_map and value_map[day] is not None else None,
"quality_flag": QUALITY_REAL if day in value_map and value_map[day] is not None else QUALITY_MISSING,
}
for day in days
]
return _apply_linear_interpolation(raw_points)
2026-03-22 01:09:09 +03:30
2026-03-22 03:08:27 +03:30
def _average_known(points: list[dict[str, Any]]) -> float | None:
values = [point["value"] for point in points if point["value"] is not None]
if not values:
return None
return sum(values) / len(values)
2026-03-22 01:09:09 +03:30
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")
2026-03-22 03:08:27 +03:30
current_value = getattr(current_sensor, "soil_moisture", None) if current_sensor is not None else None
current_value = round(float(current_value), 2) if current_value is not None else None
2026-03-22 01:09:09 +03:30
2026-03-22 03:08:27 +03:30
this_week_points = _build_week_points(history, "soil_moisture", day_offset_start=0)
last_week_points = _build_week_points(history, "soil_moisture", day_offset_start=7)
2026-03-22 01:09:09 +03:30
2026-03-22 03:08:27 +03:30
this_week_avg = _average_known(this_week_points)
last_week_avg = _average_known(last_week_points)
delta = 0
if this_week_avg is not None and last_week_avg not in (None, 0):
delta = round(((this_week_avg - last_week_avg) / last_week_avg) * 100)
2026-03-22 01:09:09 +03:30
2026-03-22 03:08:27 +03:30
categories = [_day_label(day) for day in _day_categories()]
2026-03-22 01:09:09 +03:30
return {
"currentValue": current_value,
2026-03-22 03:08:27 +03:30
"currentValueQuality": QUALITY_REAL if current_value is not None else QUALITY_MISSING,
2026-03-22 01:09:09 +03:30
"vsLastWeek": f"{'+' if delta >= 0 else ''}{delta}%",
"vsLastWeekValue": delta,
"categories": categories,
"series": [
2026-03-22 03:08:27 +03:30
{
"name": "هفته جاری",
"data": [point["value"] for point in this_week_points],
"points": this_week_points,
},
{
"name": "هفته قبل",
"data": [point["value"] for point in last_week_points],
"points": last_week_points,
},
2026-03-22 01:09:09 +03:30
],
2026-03-22 03:08:27 +03:30
"qualityLegend": {
QUALITY_REAL: "اندازه‌گیری واقعی سنسور",
QUALITY_INTERPOLATED: "برآورد خطی برای شکاف کوتاه داده",
QUALITY_MISSING: "داده معتبر در دسترس نیست",
},
2026-03-22 01:09:09 +03:30
}