Weather info from Open Meteo Forcast

89 views
Skip to first unread message

pepe

unread,
Jul 21, 2026, 7:16:31 PMJul 21
to golden-cheetah-users
I create a script to get past Open-Meteo forcasts with a 15' resolution, works for the last 14 days-
````
import urllib.request
import json
from datetime import datetime, timedelta
import time

# ----------------------------------------------------------------------
# NO LOG FILE – console & GC.log only
# ----------------------------------------------------------------------
def log(msg):
    ts = datetime.now().strftime('%H:%M:%S')
    full = f"[{ts}] {msg}"
    print(full)
    try:
        GC.log(msg + "\n")
    except Exception:
        pass

BASE_URL = "https://api.open-meteo.com/v1/forecast"

def fetch_with_retry(url, retries=5, timeout=60):
    headers = {"User-Agent": "Mozilla/5.0"}
    for attempt in range(1, retries + 1):
        try:
            req = urllib.request.Request(url, headers=headers)
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return json.loads(resp.read().decode())
        except Exception as e:
            log(f"Attempt {attempt}/{retries} failed: {e}")
            if attempt < retries:
                time.sleep(2 ** attempt)
            else:
                raise
    return None

def main():
    log("--- Weather Export (timezone=auto) ---")

    try:
        # Activity metrics
        act_date = GC.activityMetrics()["date"]
        act_time = GC.activityMetrics()["time"]
        act_duration = float(GC.activityMetrics()["Duration"])
        log(f"Activity: {act_date} {act_time}  Duration: {act_duration:.0f}s")
    except Exception as e:
        log(f"Error reading activity: {e}")
        return

    # Delete old tab
    try:
        GC.deleteXData("WEATHER")
        log("Removed existing WEATHER tab.")
    except Exception:
        pass

    # GPS average
    lat_raw = GC.series(GC.SERIES_LAT)
    lon_raw = GC.series(GC.SERIES_LON)
    lat = [float(x) for x in lat_raw if x is not None and float(x) != 0.0]
    lon = [float(x) for x in lon_raw if x is not None and float(x) != 0.0]
    if not lat or not lon:
        log("No valid GPS data.")
        return
    avg_lat, avg_lon = sum(lat) / len(lat), sum(lon) / len(lon)
    log(f"GPS center: {avg_lat:.4f}, {avg_lon:.4f}")

    # Build URL
    vars_list = [
        "temperature_2m", "relative_humidity_2m", "apparent_temperature",
        "dewpoint_2m", "precipitation", "surface_pressure",
        "wind_speed_10m", "wind_direction_10m", "wind_gusts_10m", "uv_index"
    ]
    url = (f"{BASE_URL}?latitude={avg_lat:.6f}&longitude={avg_lon:.6f}"
           f"&minutely_15={','.join(vars_list)}&past_days=90&timezone=auto")
    log("Fetching data...")

    try:
        data = fetch_with_retry(url)
    except Exception as e:
        log(f"Fetch failed: {e}")
        return

    if not data:
        log("No data returned.")
        return

    detected_tz = data.get('timezone', 'unknown')
    log(f"Detected timezone: {detected_tz}")

    minutely = data.get('minutely_15')
    if not minutely:
        log("Missing minutely_15 data.")
        return

    times = minutely.get('time')
    if not times:
        log("No time data.")
        return

    # Parse start time
    try:
        h, m, _ = map(int, str(act_time).split(':')[:3])
    except:
        h = m = 0
    start = datetime.combine(act_date, datetime.min.time()) + timedelta(hours=h, minutes=m)
    end = start + timedelta(seconds=act_duration)
    log(f"Local range: {start} → {end}")

    # Collect points (15 min before to 15 min after)
    known = []
    margin = timedelta(minutes=15)
    for i, tstr in enumerate(times):
        dt = datetime.fromisoformat(tstr)
        if dt.date() != act_date:
            continue
        if dt < start - margin or dt > end + margin:
            continue
        elapsed = (dt - start).total_seconds()
        known.append((
            elapsed,
            float(minutely.get('temperature_2m', [0])[i] or 0),
            float(minutely.get('relative_humidity_2m', [0])[i] or 0),
            float(minutely.get('apparent_temperature', [0])[i] or 0),
            float(minutely.get('dewpoint_2m', [0])[i] or 0),
            float(minutely.get('precipitation', [0])[i] or 0),
            float(minutely.get('surface_pressure', [0])[i] or 0),
            float(minutely.get('wind_speed_10m', [0])[i] or 0),
            float(minutely.get('wind_direction_10m', [0])[i] or 0),
            float(minutely.get('wind_gusts_10m', [0])[i] or 0),
            float(minutely.get('uv_index', [0])[i] or 0)
        ))

    if len(known) < 2:
        log("Not enough weather points.")
        return

    known.sort(key=lambda p: p[0])

    # Interpolate sec=0
    idx = 0
    while idx < len(known) and known[idx][0] < 0:
        idx += 1

    if idx > 0:
        prev = known[idx - 1]
        log(f"Previous point: sec={prev[0]:.0f}, temp={prev[1]:.1f}")

    if idx < len(known) and known[idx][0] == 0:
        sec0 = known[idx]
    else:
        if idx > 0 and idx < len(known):
            p0, p1 = known[idx - 1], known[idx]
            x0, x1 = p0[0], p1[0]
            def interp(j):
                return p0[j] + (p1[j] - p0[j]) * (0 - x0) / (x1 - x0) if x1 != x0 else p0[j]
            sec0 = (0.0, interp(1), interp(2), interp(3), interp(4),
                    interp(5), interp(6), interp(7), interp(8), interp(9), interp(10))
            log("Interpolated sec=0 from previous & next.")
        else:
            log("Cannot interpolate sec=0.")
            return

    # Build final points
    points = [sec0]
    for p in known:
        if p[0] > 0:
            capped = min(p[0], act_duration)
            points.append((capped,) + p[1:])

    points.sort(key=lambda p: p[0])
    # Remove duplicates
    unique = []
    seen = set()
    for p in points:
        if p[0] not in seen:
            seen.add(p[0])
            unique.append(p)
    points = unique

    if points[0][0] != 0:
        points.insert(0, sec0)

    log(f"Writing {len(points)} points.")

    # Create XData tab
    tab = "WEATHER"
    fields = [
        ("secs", "s"),
        ("TEMPERATURE", "C"),
        ("HUMIDITY", "%"),
        ("APPARENT_TEMP", "C"),
        ("DEWPOINT", "C"),
        ("PRECIPITATION", "mm"),
        ("SURFACE_PRESSURE", "hPa"),
        ("WINDSPEED", "km/h"),
        ("WIND_DIRECTION", "°"),
        ("WIND_GUSTS", "km/h"),
        ("UV_INDEX", "index")
    ]
    for name, unit in fields:
        GC.createXDataSeries(tab, name, unit)

    # Populate with rounding
    for pt in points:
        sec = int(pt[0])
        vals = list(pt[1:])
        secs_series = GC.xdataSeries(tab, "secs")
        secs_series.append(sec)
        idx = len(secs_series) - 1

        for j, (name, _) in enumerate(fields[1:]):
            val = vals[j]
            if name == "HUMIDITY":
                val = round(val)
            else:
                val = round(val, 1)
            series = GC.xdataSeries(tab, name)
            while len(series) <= idx:
                series.append(0.0)
            series[idx] = val

    # Save
    try:
        GC.activitySave()
    except:
        pass

    log(f"✅ Wrote {len(points)} points to 'WEATHER' tab.")
    log(f"Timezone: {detected_tz}")

if __name__ == "__main__":
    main()
````

Nigel Laws

unread,
Jul 25, 2026, 7:46:21 AM (13 days ago) Jul 25
to golden-cheetah-users

I have been after something like this for a long time, with no luck, the other examples of such here do not seem to do anything so I gave up, unless they have not been updated to the latest versions of GC.

But, unless I am doing something wrong, I cannot get it to work and get a wrong "Syntax error Wrong  Malformed Expression" sometimes depending on how I try to get it to work. Could it be that "Tick Mark" in a "green square" near the bottom?

Many thanks for any assistance to get it to work

Nigel Laws

unread,
Jul 25, 2026, 7:55:44 AM (13 days ago) Jul 25
to golden-cheetah-users
Seems to be "Line 5"  "Syntax Error"

pepe

unread,
Jul 25, 2026, 2:05:42 PM (13 days ago) Jul 25
to golden-cheetah-users
Sorry,  try :
Py.txt

pepe

unread,
Jul 25, 2026, 2:12:03 PM (13 days ago) Jul 25
to golden-cheetah-users
to prevent Linux/Mac crashes:
Py8.txt

pepe

unread,
Jul 25, 2026, 7:08:39 PM (13 days ago) Jul 25
to golden-cheetah-users
Final script: Open Meteo weather data fetcher
Purpose: Import Open‑Meteo weather data for GPS activities # activity using 15‑minute buckets with GPS averaging. (configurable) # Includes explicit pre/post fetches for accurate # boundary interpolation at sec=0 and sec=duration.
Open Meteo weather data fetcher.txt
Reply all
Reply to author
Forward
0 new messages