#!/usr/bin/env python3 """The live sky over a chosen city (default Tokyo) breathing into the pads. Temperature, wind, rain, barometric pressure and humidity become slow control signals -- weather is inherently musical: it drifts, swells, and never repeats. """ import argparse import json import sys import time import urllib.error import urllib.request from pythonosc.udp_client import SimpleUDPClient NAME = "world_weather" POLL_INTERVAL = 300.0 # seconds between real API polls REEMIT_INTERVAL = 5.0 # seconds between re-emitting last known values USER_AGENT = "Godstrument/1.0 (world_weather worker)" # open-meteo "current" key -> OSC address suffix FIELDS = { "temperature_2m": "/gs/weather/temp", "wind_speed_10m": "/gs/weather/wind", "precipitation": "/gs/weather/precip", "surface_pressure": "/gs/weather/pressure", "relative_humidity_2m": "/gs/weather/humidity", } def fetch_current(lat, lon): """Fetch the 'current' block from open-meteo. Returns a dict or None.""" params = ",".join(FIELDS.keys()) url = ( "https://api.open-meteo.com/v1/forecast" f"?latitude={lat}&longitude={lon}¤t={params}" ) req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=20) as resp: data = json.loads(resp.read().decode("utf-8")) return data.get("current") or {} def main(): ap = argparse.ArgumentParser(description="Weather -> OSC control signals.") ap.add_argument("--host", default="127.0.0.1") ap.add_argument("--port", type=int, default=9000) ap.add_argument("--lat", type=float, default=35.68, help="latitude (default Tokyo)") ap.add_argument("--lon", type=float, default=139.69, help="longitude (default Tokyo)") args = ap.parse_args() client = SimpleUDPClient(args.host, args.port) print(f"[{NAME}] emitting /gs/weather/... every {int(REEMIT_INTERVAL)}s") last = {} # OSC address -> last known float next_poll = 0.0 # poll immediately on first loop backoff = 5.0 # network retry backoff, grows on repeated failure while True: now = time.monotonic() if now >= next_poll: try: current = fetch_current(args.lat, args.lon) for key, addr in FIELDS.items(): if key in current and current[key] is not None: last[addr] = float(current[key]) next_poll = now + POLL_INTERVAL backoff = 5.0 except (urllib.error.URLError, OSError, ValueError, json.JSONDecodeError) as e: print(f"[{NAME}] poll failed: {e}; retry in {backoff:.0f}s") next_poll = now + backoff backoff = min(backoff * 2, POLL_INTERVAL) # Re-emit last known values so downstream signals stay live. for addr, value in last.items(): try: client.send_message(addr, value) except OSError as e: print(f"[{NAME}] send failed on {addr}: {e}") time.sleep(REEMIT_INTERVAL) if __name__ == "__main__": try: main() except KeyboardInterrupt: sys.exit(0)