from __future__ import annotations import argparse from dataclasses import dataclass from os import environ from sys import stderr from time import sleep from astrape_ems.classes.env import EnvLoader from astrape_oracle.classes.weather.ecowitt import EcowittGw3000Client from astrape_oracle.classes.weather.store import WeatherStore @dataclass(frozen=True) class WeatherStationDaemonConfig: host: str poll_seconds: float timeout_seconds: float @classmethod def from_env(cls) -> "WeatherStationDaemonConfig": host = environ.get("ECOWITT_GW3000_HOST") if not host: raise RuntimeError("ECOWITT_GW3000_HOST is required") return cls( host=host, poll_seconds=float(environ.get("ECOWITT_GW3000_POLL_SECONDS", "60")), timeout_seconds=float(environ.get("ECOWITT_GW3000_TIMEOUT_SECONDS", "5")), ) class WeatherStationDaemon: """Polls local weather station observations into TimescaleDB.""" def __init__( self, config: WeatherStationDaemonConfig, client: EcowittGw3000Client, weather_store: WeatherStore, ) -> None: self.config = config self.client = client self.weather_store = weather_store @classmethod def from_env(cls) -> "WeatherStationDaemon": config = WeatherStationDaemonConfig.from_env() return cls( config=config, client=EcowittGw3000Client( host=config.host, timeout_seconds=config.timeout_seconds, ), weather_store=WeatherStore.from_env(), ) def initialize(self) -> None: self.weather_store.initialize() def run_once(self) -> int: observation = self.client.fetch_observation() return self.weather_store.save_station_observation(observation) def run_forever(self) -> None: self.initialize() while True: try: saved_count = self.run_once() print( f"stored_weather_station_observations={saved_count}", flush=True, ) except Exception as error: print(f"weather_station_poll_error={error}", file=stderr, flush=True) sleep(self.config.poll_seconds) def main() -> None: try: EnvLoader().load() daemon = WeatherStationDaemon.from_env() args = parse_args() if args.once: daemon.initialize() saved_count = daemon.run_once() print( f"stored_weather_station_observations={saved_count}", flush=True, ) return daemon.run_forever() except Exception as error: print(f"weather_station_daemon_startup_error={error}", file=stderr) raise SystemExit(1) from error def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Poll local Ecowitt weather station observations into Astrape." ) parser.add_argument( "--once", action="store_true", help="Initialize storage, save one observation, and exit.", ) return parser.parse_args() if __name__ == "__main__": main()