Initial commit: astrape-oracle split out of the combined Astrape tree
The forecasting runtime gibil pilots: weather ingest (Open-Meteo forecasts and the Ecowitt station), the oracle and weather stores, the snapshot daemon, the forecast evaluator, and the TCN training scheduler. classes/datasets.py holds the shapes the stores return for display. They sit beside the stores deliberately: a store's return type belongs to the store, and keeping them with the renderers would mean this repo importing the dashboard, which is backwards. Extracted from a single repository where the forecasting engine, the database plumbing, the hardware client, the ingest daemons and the web UI all imported each other freely. Verified that every module imports with gibil resolved from its own installed checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user