from __future__ import annotations from datetime import datetime, timezone from gibil.classes.models import Observation, ObservationQuality, Snapshot class SnapshotBuilder: """Builds Gibil's decision input from the latest observations.""" def build(self, observations: list[Observation]) -> Snapshot: latest = self._latest_by_metric(observations) return Snapshot( created_at=datetime.now(timezone.utc), solar_power_w=self._number(latest, "solar_power_w"), home_power_w=self._number(latest, "home_power_w"), battery_soc_pct=self._number(latest, "battery_soc_pct"), grid_import_w=self._number(latest, "grid_import_w"), grid_export_w=self._number(latest, "grid_export_w"), cheap_window_active=self._boolean(latest, "cheap_window_active"), input_quality={ metric: observation.quality for metric, observation in latest.items() }, ) def _latest_by_metric( self, observations: list[Observation] ) -> dict[str, Observation]: latest: dict[str, Observation] = {} for observation in observations: existing = latest.get(observation.metric) if existing is None or observation.observed_at > existing.observed_at: latest[observation.metric] = observation return latest def _number( self, observations: dict[str, Observation], metric: str ) -> float | None: observation = observations.get(metric) if observation is None or observation.quality != ObservationQuality.OK: return None if isinstance(observation.value, bool): return None if isinstance(observation.value, int | float): return float(observation.value) return None def _boolean(self, observations: dict[str, Observation], metric: str) -> bool: observation = observations.get(metric) if observation is None or observation.quality != ObservationQuality.OK: return False if isinstance(observation.value, bool): return observation.value return False