Files
astrape-oracle/scripts/daemons/weather_station_daemon.py
rpotter6298 9a4ee8e0cf Flatten to a single package directory
The checkout is now astrape/astrape-oracle, not astrape/astrape-oracle/<package>. The package
contents move to the repository root and pyproject maps the package name to
'.' via package-dir, with the subpackages listed explicitly.

The systemd units move out to /srv/astrape/deploy/systemd, outside every
repository, because they describe this deployment rather than any one
component.

Verified both install modes: editable, which is how the deployment runs, and
a plain wheel install, which is what the container build does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 15:05:17 +02:00

114 lines
3.2 KiB
Python

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