Godstrument/workers/world_weather.py
monsterrobotparty 8deb65b4fe Godstrument: a multi-modal sensor-fusion instrument
Everything — your hand, the room, a satellite, the markets, the sky — becomes an
OSC control signal fused through a modulation matrix. Includes:
- hub + normalize (One-Euro) + matrix (curves/gates/quantize) + transform (tweaks/groups)
- 20+ workers: sim sensors, 8 keyless world feeds, ephemeris/almanac/clock (computed),
  time-warp replay (quakes/weather/db), and the dealgod market warehouse feed
- planetary orbital LFOs, SQLite recorder, city targeting
- live browser console: mute, group macros, drag-to-patch, inspector, Web MIDI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 02:13:05 +10:00

88 lines
3.1 KiB
Python

#!/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}&current={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)