""" replay_weather.py — days of sky, sweeping past in minutes. Fetches hourly weather history for the venue from Open-Meteo's archive, then replays it compressed onto a loop (default: 7 days -> 4 minutes) so slow weather finally moves fast enough to shape a track. Jog-scrubbable like the quake replay. Drop-in replacement for world_weather.py — emits the same /gs/weather/* addresses (temp, wind, precip, pressure, humidity) as smoothly interpolated sweeps. """ from __future__ import annotations import argparse import datetime import json import threading import time import urllib.request from pythonosc.udp_client import SimpleUDPClient from pythonosc.dispatcher import Dispatcher from pythonosc.osc_server import ThreadingOSCUDPServer import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from timewarp import ReplayBuffer UA = "Godstrument/1.0 (live instrument)" FIELDS = { "temperature_2m": "temp", "wind_speed_10m": "wind", "precipitation": "precip", "surface_pressure": "pressure", "relative_humidity_2m": "humidity", } class Jog: def __init__(self, rate_min=0.25, rate_max=4.0): self.rate_min, self.rate_max = rate_min, rate_max self.rate_mult = 1.0 self.nudge = 0.0 def on_rate(self, addr, *a): if a: v = max(0.0, min(1.0, float(a[0]))) self.rate_mult = self.rate_min + (self.rate_max - self.rate_min) * v def on_nudge(self, addr, *a): if a: self.nudge = (max(0.0, min(1.0, float(a[0]))) - 0.5) * 2.0 def fetch(lat: float, lon: float, days: int): end = datetime.datetime.now(datetime.timezone.utc).date() start = end - datetime.timedelta(days=days) hourly = ",".join(FIELDS.keys()) url = ("https://archive-api.open-meteo.com/v1/archive" f"?latitude={lat}&longitude={lon}" f"&start_date={start}&end_date={end}&hourly={hourly}") req = urllib.request.Request(url, headers={"User-Agent": UA}) with urllib.request.urlopen(req, timeout=30) as r: data = json.load(r) h = data["hourly"] n = len(h["time"]) times = list(range(n)) # uniform hourly index is fine for phase mapping return times, {out: h.get(api, []) for api, out in FIELDS.items()} def main(): ap = argparse.ArgumentParser() ap.add_argument("--host", default="127.0.0.1") ap.add_argument("--port", type=int, default=9000) ap.add_argument("--jog-port", type=int, default=9102) ap.add_argument("--lat", type=float, default=35.68) ap.add_argument("--lon", type=float, default=139.69) ap.add_argument("--days", type=int, default=7) ap.add_argument("--into", type=float, default=240.0, help="loop length (s)") args = ap.parse_args() client = SimpleUDPClient(args.host, args.port) jog = Jog() bufs: dict[str, ReplayBuffer] = {} while not bufs: try: times, series = fetch(args.lat, args.lon, args.days) for name, vals in series.items(): b = ReplayBuffer(mode="continuous", into_seconds=args.into) b.load_continuous(times, vals) if b.loaded: bufs[name] = b print(f"[replay_weather] {len(times)}h of weather ({args.lat},{args.lon}) " f"-> looping every {args.into:g}s (jog udp/{args.jog_port})") except Exception as e: # noqa print(f"[replay_weather] fetch failed ({e}); retrying in 10s") time.sleep(10) disp = Dispatcher() disp.map("/jog/rate", jog.on_rate) disp.map("/jog/nudge", jog.on_nudge) srv = ThreadingOSCUDPServer(("127.0.0.1", args.jog_port), disp) threading.Thread(target=srv.serve_forever, daemon=True).start() period = 1.0 / 30.0 while True: for name, b in bufs.items(): b.tick(period, jog.rate_mult, jog.nudge) client.send_message(f"/gs/weather/{name}", float(b.current())) time.sleep(period) if __name__ == "__main__": main()