Choosing room and weather freshness limits required knowing how long each sensor normally goes without reporting, which HA does not show. Add max_age_seconds_24h to every source in input_quality: the largest report age seen at polls over the last 24 hours (hourly buckets, reset on reload, underestimates by up to one poll interval). input_quality and weather_comparison change every poll, so the Recorder stored a new attribute row every five minutes. Mark them unrecorded; they remain visible on the Status entity. Model coefficients and other attributes are still recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
40 lines
1.6 KiB
Python
40 lines
1.6 KiB
Python
"""Pure timestamp validation for reported sensor snapshots."""
|
|
from __future__ import annotations
|
|
|
|
from collections import deque
|
|
from datetime import datetime
|
|
from math import isfinite
|
|
|
|
|
|
def freshness(reported: datetime | None, now: datetime, max_age_seconds: float) -> dict:
|
|
"""Use report time, not value-change time; reject unknown/future clocks."""
|
|
if not isfinite(max_age_seconds) or max_age_seconds <= 0:
|
|
raise ValueError("Freshness limit must be positive and finite")
|
|
if not isinstance(reported, datetime) or reported.tzinfo is None:
|
|
return {"status": "invalid_timestamp", "age_seconds": None}
|
|
age = (now - reported).total_seconds()
|
|
if age < 0:
|
|
return {"status": "future_timestamp", "age_seconds": round(age, 1)}
|
|
return {"status": "stale" if age > max_age_seconds else "valid",
|
|
"age_seconds": round(age, 1)}
|
|
|
|
|
|
class ReportAgeHistory:
|
|
"""Largest report age seen at polls per entity over the last 24 hours.
|
|
|
|
Helps choose freshness limits: a limit below a sensor's normal maximum
|
|
marks it stale during ordinary operation. Hourly buckets keep memory bounded.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._buckets: dict[str, deque[list[float]]] = {}
|
|
|
|
def observe(self, entity_id: str, now: datetime, age_seconds: float) -> float:
|
|
hour = now.timestamp() // 3600
|
|
buckets = self._buckets.setdefault(entity_id, deque(maxlen=25))
|
|
if buckets and buckets[-1][0] == hour:
|
|
buckets[-1][1] = max(buckets[-1][1], age_seconds)
|
|
else:
|
|
buckets.append([hour, age_seconds])
|
|
return max(age for start, age in buckets if hour - 24 < start <= hour)
|