from __future__ import annotations import os from dataclasses import dataclass from zoneinfo import ZoneInfo, ZoneInfoNotFoundError def _float_env(name: str, default: float, minimum: float) -> float: raw = os.getenv(name, str(default)) try: value = float(raw) except ValueError as exc: raise ValueError(f"{name} must be a number") from exc if value < minimum: raise ValueError(f"{name} must be at least {minimum}") return value def _int_env(name: str, default: int, minimum: int) -> int: raw = os.getenv(name, str(default)) try: value = int(raw) except ValueError as exc: raise ValueError(f"{name} must be an integer") from exc if value < minimum: raise ValueError(f"{name} must be at least {minimum}") return value @dataclass(frozen=True) class Settings: tapo_host: str tapo_username: str tapo_password: str timezone_name: str poll_interval_seconds: float start_watts: float stop_watts: float confirm_samples: int max_gap_seconds: float sample_retention_days: int database_path: str dashboard_username: str dashboard_password: str @classmethod def from_env(cls) -> "Settings": timezone_name = os.getenv("TZ", "Europe/Prague") try: ZoneInfo(timezone_name) except ZoneInfoNotFoundError as exc: raise ValueError(f"TZ is not a known timezone: {timezone_name}") from exc start_watts = _float_env("START_WATTS", 100.0, 0.1) stop_watts = _float_env("STOP_WATTS", 30.0, 0.0) if stop_watts >= start_watts: raise ValueError("STOP_WATTS must be lower than START_WATTS") poll_interval = _float_env("POLL_INTERVAL_SECONDS", 2.0, 0.5) max_gap = _float_env("MAX_GAP_SECONDS", max(20.0, poll_interval * 5), poll_interval * 2) return cls( tapo_host=os.getenv("TAPO_HOST", "").strip(), tapo_username=os.getenv("TAPO_USERNAME", "").strip(), tapo_password=os.getenv("TAPO_PASSWORD", ""), timezone_name=timezone_name, poll_interval_seconds=poll_interval, start_watts=start_watts, stop_watts=stop_watts, confirm_samples=_int_env("CONFIRM_SAMPLES", 2, 1), max_gap_seconds=max_gap, sample_retention_days=_int_env("SAMPLE_RETENTION_DAYS", 30, 1), database_path=os.getenv("DATABASE_PATH", "/data/pump-monitor.sqlite3"), dashboard_username=os.getenv("DASHBOARD_USERNAME", "water-monitor").strip(), dashboard_password=os.getenv("DASHBOARD_PASSWORD", ""), ) @property def credentials_configured(self) -> bool: return bool(self.tapo_username and self.tapo_password) @property def timezone(self) -> ZoneInfo: return ZoneInfo(self.timezone_name)