393 files

This commit is contained in:
Home Assistant Version Control
2026-09-27 17:16:54 +00:00
parent b05b7a897e
commit 39d97ac2db
404 changed files with 54006 additions and 11034 deletions
@@ -35,9 +35,31 @@ from ..const import (
MaintenanceFeedback,
)
from .history import completed_entries
from .least_squares import linear_fit
from .schedule import read_legacy_fields
def month_overrides(raw: Any) -> dict[int, float] | None:
"""Manual seasonal factors as ``{month 1-12: factor}``.
Stored with int keys, but the JSON store hands them back as strings
after a restart — ``1 <= "7"`` then raised in the analysis (swallowed by
the coordinator, so adaptive scheduling silently died for the task; bug
audit 2026-09-26). Invalid entries are dropped.
"""
if not isinstance(raw, dict):
return None
out: dict[int, float] = {}
for key, value in raw.items():
try:
month, factor = int(key), float(value)
except (TypeError, ValueError):
continue
if 1 <= month <= 12 and math.isfinite(factor):
out[month] = factor
return out or None
def hemisphere(hass: Any) -> str:
"""``"south"`` / ``"north"`` from HA's configured latitude — the seasonal
anchor the adaptive analysis takes. ``None`` (an un-onboarded HA) reads
@@ -189,7 +211,7 @@ class IntervalAnalyzer:
if recommended is not None and seasonal_enabled:
intervals_with_months = self._compute_intervals_with_months(history)
hemisphere = adaptive_config.get("hemisphere", "north")
manual_overrides = adaptive_config.get("seasonal_overrides")
manual_overrides = month_overrides(adaptive_config.get("seasonal_overrides"))
current_month = adaptive_config.get("_current_month") or dt_util.now().month
@@ -436,18 +458,13 @@ class IntervalAnalyzer:
# Least-squares regression: y = beta * x - beta * ln(eta)
# => y = m * x + b, where m = beta, b = -beta * ln(eta)
n_pts = len(x_vals)
sum_x = sum(x_vals)
sum_y = sum(y_vals)
sum_xy = sum(x * y for x, y in zip(x_vals, y_vals, strict=True))
sum_x2 = sum(x * x for x in x_vals)
denom = n_pts * sum_x2 - sum_x * sum_x
if abs(denom) < 1e-10:
# (the shared fit — the sensor predictor carried a second copy).
fit = linear_fit(x_vals, y_vals, min_denom=1e-10)
if fit is None:
return None
beta = (n_pts * sum_xy - sum_x * sum_y) / denom
b = (sum_y - beta * sum_x) / n_pts
beta, b = fit
n_pts = len(x_vals)
sum_y = sum(y_vals)
if beta <= 0:
return None