#!/usr/bin/env python3 """A city's pollution turned into audible grit and glitch. Real-time PM2.5/PM10/US-AQI from Open-Meteo's air-quality API for a chosen city. Dirtier air -> more grit; spikes glitch the signal chain. Delhi by default. """ import argparse import json import time import urllib.request import urllib.error from pythonosc.udp_client import SimpleUDPClient NAME = "world_air" POLL = 300.0 # seconds between API polls (respect the source) REEMIT = 5.0 # re-emit last known values this often USER_AGENT = "Godstrument/1.0 (+worker world_air)" URL = ("https://air-quality-api.open-meteo.com/v1/air-quality" "?latitude={lat}&longitude={lon}¤t=pm2_5,pm10,us_aqi") def fetch(lat, lon): """Return (pm25, pm10, aqi) as floats, or None on any failure.""" url = URL.format(lat=lat, lon=lon) 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")) cur = data["current"] pm25 = float(cur["pm2_5"]) pm10 = float(cur["pm10"]) aqi = float(cur["us_aqi"]) return pm25, pm10, aqi def main(): ap = argparse.ArgumentParser(description="Air-quality -> OSC grit worker.") ap.add_argument("--host", default="127.0.0.1") ap.add_argument("--port", type=int, default=9000) ap.add_argument("--lat", type=float, default=28.61, help="latitude (default Delhi)") ap.add_argument("--lon", type=float, default=77.20, help="longitude (default Delhi)") args = ap.parse_args() client = SimpleUDPClient(args.host, args.port) print(f"[{NAME}] emitting /gs/air/... every {int(REEMIT)}s") last = None # (pm25, pm10, aqi) once we have a reading next_poll = 0.0 # poll immediately on startup backoff = 5.0 while True: now = time.monotonic() if now >= next_poll: try: last = fetch(args.lat, args.lon) next_poll = now + POLL backoff = 5.0 except (urllib.error.URLError, urllib.error.HTTPError, OSError, ValueError, KeyError, TypeError) as e: print(f"[{NAME}] poll failed ({e}); retrying in {backoff:.0f}s") next_poll = now + backoff backoff = min(backoff * 2, POLL) if last is not None: pm25, pm10, aqi = last try: client.send_message("/gs/air/pm25", float(pm25)) client.send_message("/gs/air/pm10", float(pm10)) client.send_message("/gs/air/aqi", float(aqi)) except OSError as e: print(f"[{NAME}] OSC send failed ({e}); continuing") time.sleep(REEMIT) if __name__ == "__main__": main()