393 files
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user