Skip to content

openmeteo

openmeteo #

Hourly weather forecasts from Open-Meteo (free, no API key).

CACHE_EXPIRY_SECONDS = 3600 module-attribute #

How long a fetched forecast stays fresh in the HTTP cache.

HOURLY_VARIABLES = ('temperature_2m', 'apparent_temperature', 'dew_point_2m', 'relative_humidity_2m', 'pressure_msl', 'wind_speed_10m', 'wind_gusts_10m', 'wind_direction_10m', 'cloud_cover', 'precipitation', 'visibility', 'uv_index', 'is_day', 'weather_code') module-attribute #

The Open-Meteo hourly variables requested, in response order.

UNKNOWN_CONDITION = ('Unknown', 'cloud.fill') module-attribute #

What a WMO code outside the table reports.

WMO_CONDITIONS = {0: ('Clear', 'sun.max.fill'), 1: ('Mainly Clear', 'sun.max.fill'), 2: ('Partly Cloudy', 'cloud.sun.fill'), 3: ('Overcast', 'cloud.fill'), 45: ('Fog', 'cloud.fog.fill'), 48: ('Depositing Rime Fog', 'cloud.fog.fill'), 51: ('Light Drizzle', 'cloud.drizzle.fill'), 53: ('Moderate Drizzle', 'cloud.drizzle.fill'), 55: ('Dense Drizzle', 'cloud.drizzle.fill'), 61: ('Slight Rain', 'cloud.rain.fill'), 63: ('Moderate Rain', 'cloud.rain.fill'), 65: ('Heavy Rain', 'cloud.heavyrain.fill'), 71: ('Slight Snow', 'cloud.snow.fill'), 73: ('Moderate Snow', 'cloud.snow.fill'), 75: ('Heavy Snow', 'cloud.snow.fill'), 77: ('Snow Grains', 'cloud.snow.fill'), 80: ('Slight Rain Showers', 'cloud.rain.fill'), 81: ('Moderate Rain Showers', 'cloud.rain.fill'), 82: ('Violent Rain Showers', 'cloud.heavyrain.fill'), 85: ('Slight Snow Showers', 'cloud.snow.fill'), 86: ('Heavy Snow Showers', 'cloud.snow.fill'), 95: ('Thunderstorm', 'cloud.bolt.rain.fill'), 96: ('Thunderstorm with Slight Hail', 'cloud.bolt.rain.fill'), 99: ('Thunderstorm with Heavy Hail', 'cloud.bolt.rain.fill')} module-attribute #

Each WMO weather code as its condition name and SF Symbol.

default_cache_path() #

Where Open-Meteo responses are cached when no path is given.

Source code in capturegraph-lib/capturegraph/scheduling/forecast/openmeteo.py
def default_cache_path() -> Path:
    """Where Open-Meteo responses are cached when no path is given."""
    root = os.environ.get("XDG_CACHE_HOME")
    return (Path(root) if root else Path.home() / ".cache") / "capturegraph" / "open-meteo"

hourly_weather(location, days=3, cache_path=None) #

Fetch an hourly weather forecast from Open-Meteo.

Parameters:

Name Type Description Default
location Location

The place to forecast for.

required
days int

Number of days to forecast (1-16). Defaults to 3.

3
cache_path Path | None

Where to cache the HTTP responses. Defaults to [default_cache_path][] under the user's cache directory.

None

Returns:

Type Description
Array[Weather]

One cg.Weather per forecast hour.

Raises:

Type Description
ImportError

If openmeteo-requests is not installed.

RuntimeError

If the response carries no hourly data.

Source code in capturegraph-lib/capturegraph/scheduling/forecast/openmeteo.py
def hourly_weather(
    location: cg.Location,
    days: int = 3,
    cache_path: Path | None = None,
) -> cg.Array[cg.Weather]:
    """Fetch an hourly weather forecast from Open-Meteo.

    Args:
        location: The place to forecast for.
        days: Number of days to forecast (1-16). Defaults to 3.
        cache_path: Where to cache the HTTP responses. Defaults to
            [default_cache_path][] under the user's cache directory.

    Returns:
        One ``cg.Weather`` per forecast hour.

    Raises:
        ImportError: If openmeteo-requests is not installed.
        RuntimeError: If the response carries no hourly data.
    """
    try:
        import openmeteo_requests
        import requests_cache
        from retry_requests import retry
    except ImportError as error:
        raise ImportError(
            "hourly_weather requires openmeteo-requests. Install with: "
            "uv add openmeteo-requests requests-cache retry-requests"
        ) from error

    cache = cache_path if cache_path is not None else default_cache_path()
    cache.parent.mkdir(parents=True, exist_ok=True)
    session = requests_cache.CachedSession(str(cache), expire_after=CACHE_EXPIRY_SECONDS)
    # The openmeteo client's stub types ``session`` as its own Session class, which
    # the retry-wrapped CachedSession does not statically match; it works at runtime.
    client = openmeteo_requests.Client(session=retry(session, retries=5, backoff_factor=0.2))  # pyright: ignore[reportArgumentType]

    responses = client.weather_api(
        "https://api.open-meteo.com/v1/forecast",
        params={
            "latitude": location.latitude,
            "longitude": location.longitude,
            "forecast_days": min(days, 16),
            "hourly": list(HOURLY_VARIABLES),
        },
    )

    hourly = responses[0].Hourly()
    if hourly is None:
        raise RuntimeError("Open-Meteo response returned no hourly data")

    def values(name: str) -> np.ndarray:
        variable = hourly.Variables(HOURLY_VARIABLES.index(name))
        if variable is None:
            raise RuntimeError(f"Open-Meteo hourly variable {name} is missing")
        return variable.ValuesAsNumpy()

    temperature = values("temperature_2m")
    apparent_temperature = values("apparent_temperature")
    dew_point = values("dew_point_2m")
    humidity = values("relative_humidity_2m")
    pressure = values("pressure_msl")
    wind_speed = values("wind_speed_10m")
    wind_gust = values("wind_gusts_10m")
    wind_direction = values("wind_direction_10m")
    cloud_cover = values("cloud_cover")
    precipitation = values("precipitation")
    visibility = values("visibility")
    uv_index = values("uv_index")
    is_day = values("is_day")
    weather_code = values("weather_code")

    times = pd.date_range(
        start=pd.to_datetime(hourly.Time(), unit="s", utc=True),
        end=pd.to_datetime(hourly.TimeEnd(), unit="s", utc=True),
        freq=pd.Timedelta(seconds=hourly.Interval()),
        inclusive="left",
    )

    forecasts = []
    for index, moment in enumerate(times):
        condition, symbol = WMO_CONDITIONS.get(int(weather_code[index]), UNKNOWN_CONDITION)
        forecasts.append(
            cg.Weather(
                temperature_celsius=float(temperature[index]),
                apparent_temperature_celsius=float(apparent_temperature[index]),
                dew_point_celsius=float(dew_point[index]),
                humidity_ratio=float(humidity[index]) / 100.0,
                pressure_hpa=float(pressure[index]),
                wind_speed_mps=float(wind_speed[index]) / 3.6,
                wind_gust_mps=float(wind_gust[index]) / 3.6,
                wind_direction_degrees=float(wind_direction[index]),
                condition=condition,
                symbol_name=symbol,
                cloud_cover_ratio=float(cloud_cover[index]) / 100.0,
                precipitation_intensity_mmph=float(precipitation[index]),
                visibility_meters=float(visibility[index]),
                uv_index=float(uv_index[index]),
                is_daylight=bool(is_day[index]),
                time=moment.to_pydatetime().replace(tzinfo=None),
            )
        )

    return cg.Array(forecasts)