ccd4e7a48c
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>
123 lines
3.9 KiB
Python
123 lines
3.9 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 gibil.classes.oracle_builder import EnergyOracleBuilder
|
|
from gibil.classes.forecast_config import EnergyForecastConfig
|
|
from astrape_ems.classes.env import EnvLoader
|
|
from astrape_oracle.classes.oracle_store import OracleStore
|
|
from astrape_ems.classes.store import SigenStore
|
|
from astrape_oracle.classes.weather.store import WeatherStore
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OracleDaemonConfig:
|
|
poll_seconds: float
|
|
evaluate_forecasts: bool
|
|
evaluation_actual_window_minutes: float
|
|
evaluation_lookback_hours: float
|
|
evaluation_limit: int
|
|
|
|
@classmethod
|
|
def from_env(cls) -> "OracleDaemonConfig":
|
|
return cls(
|
|
poll_seconds=float(environ.get("ASTRAPE_ORACLE_POLL_SECONDS", "300")),
|
|
evaluate_forecasts=environ.get(
|
|
"ASTRAPE_ORACLE_EVALUATE_FORECASTS", "1"
|
|
).lower()
|
|
not in {"0", "false", "no"},
|
|
evaluation_actual_window_minutes=float(
|
|
environ.get("ASTRAPE_ORACLE_EVALUATION_WINDOW_MINUTES", "5")
|
|
),
|
|
evaluation_lookback_hours=float(
|
|
environ.get("ASTRAPE_ORACLE_EVALUATION_LOOKBACK_HOURS", "168")
|
|
),
|
|
evaluation_limit=int(environ.get("ASTRAPE_ORACLE_EVALUATION_LIMIT", "1000")),
|
|
)
|
|
|
|
|
|
class OracleDaemon:
|
|
"""Periodically stores oracle projection curves for evaluation."""
|
|
|
|
def __init__(
|
|
self,
|
|
config: OracleDaemonConfig,
|
|
builder: EnergyOracleBuilder,
|
|
store: OracleStore,
|
|
) -> None:
|
|
self.config = config
|
|
self.builder = builder
|
|
self.store = store
|
|
|
|
@classmethod
|
|
def from_env(cls) -> "OracleDaemon":
|
|
return cls(
|
|
config=OracleDaemonConfig.from_env(),
|
|
builder=EnergyOracleBuilder(
|
|
weather_history=WeatherStore.from_env(),
|
|
plant_history=SigenStore.from_env(),
|
|
config=EnergyForecastConfig.from_env(),
|
|
),
|
|
store=OracleStore.from_env(),
|
|
)
|
|
|
|
def run_once(self) -> int:
|
|
solar_run, load_run, net_run = self.builder.build()
|
|
saved_count = self.store.save_runs(solar_run, load_run, net_run)
|
|
if self.config.evaluate_forecasts:
|
|
from datetime import timedelta
|
|
|
|
evaluated_count = self.store.evaluate_due_forecasts(
|
|
actual_window=timedelta(
|
|
minutes=self.config.evaluation_actual_window_minutes
|
|
),
|
|
lookback=timedelta(hours=self.config.evaluation_lookback_hours),
|
|
limit=self.config.evaluation_limit,
|
|
)
|
|
return saved_count + evaluated_count
|
|
return saved_count
|
|
|
|
def run_forever(self) -> None:
|
|
self.store.initialize()
|
|
while True:
|
|
try:
|
|
saved_count = self.run_once()
|
|
print(f"stored_oracle_records={saved_count}", flush=True)
|
|
except Exception as error:
|
|
print(f"oracle_poll_error={error}", file=stderr, flush=True)
|
|
sleep(self.config.poll_seconds)
|
|
|
|
|
|
def main() -> None:
|
|
try:
|
|
EnvLoader().load()
|
|
args = parse_args()
|
|
daemon = OracleDaemon.from_env()
|
|
if args.once:
|
|
print(f"stored_oracle_records={daemon.run_once()}", flush=True)
|
|
return
|
|
daemon.run_forever()
|
|
except Exception as error:
|
|
print(f"oracle_daemon_startup_error={error}", file=stderr)
|
|
raise SystemExit(1) from error
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Store Astrape oracle projection curves."
|
|
)
|
|
parser.add_argument(
|
|
"--once",
|
|
action="store_true",
|
|
help="Save one set of oracle curves and exit.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|