126 lines
4.6 KiB
Python
126 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
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)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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 = 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
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
categories = [_day_label(day) for day in _day_categories()]
|
|
|
|
return {
|
|
"currentValue": current_value,
|
|
"currentValueQuality": QUALITY_REAL if current_value is not None else QUALITY_MISSING,
|
|
"vsLastWeek": f"{'+' if delta >= 0 else ''}{delta}%",
|
|
"vsLastWeekValue": delta,
|
|
"categories": categories,
|
|
"series": [
|
|
{
|
|
"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,
|
|
},
|
|
],
|
|
"qualityLegend": {
|
|
QUALITY_REAL: "اندازهگیری واقعی سنسور",
|
|
QUALITY_INTERPOLATED: "برآورد خطی برای شکاف کوتاه داده",
|
|
QUALITY_MISSING: "داده معتبر در دسترس نیست",
|
|
},
|
|
}
|