9a4ee8e0cf
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>
114 lines
3.2 KiB
Python
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()
|