"""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)