Files
Astrape/gibil/classes/snapshot_builder.py
T
rpotter6298 9d15860f0b first_commit
2026-04-25 20:35:25 +02:00

64 lines
2.1 KiB
Python

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