Skip to content

meteostat

meteostat #

Historical weather from Meteostat station observations.

Backfills a cg.Weather for captures that did not record one. Meteostat reports raw station units (km/h wind, % humidity, okta cloud cover) and omits apparent temperature, visibility, and UV entirely.

COCO_CONDITIONS = {1: ('Clear', 'sun.max.fill'), 2: ('Mostly Clear', 'sun.max.fill'), 3: ('Partly Cloudy', 'cloud.sun.fill'), 4: ('Cloudy', 'cloud.fill'), 5: ('Fog', 'cloud.fog.fill'), 6: ('Freezing Fog', 'cloud.fog.fill'), 7: ('Light Rain', 'cloud.drizzle.fill'), 8: ('Rain', 'cloud.rain.fill'), 9: ('Heavy Rain', 'cloud.heavyrain.fill'), 10: ('Freezing Rain', 'cloud.sleet.fill'), 11: ('Heavy Freezing Rain', 'cloud.sleet.fill'), 12: ('Sleet', 'cloud.sleet.fill'), 13: ('Heavy Sleet', 'cloud.sleet.fill'), 14: ('Light Snow', 'cloud.snow.fill'), 15: ('Snow', 'cloud.snow.fill'), 16: ('Heavy Snow', 'cloud.snow.fill'), 17: ('Rain Shower', 'cloud.rain.fill'), 18: ('Heavy Rain Shower', 'cloud.heavyrain.fill'), 19: ('Sleet Shower', 'cloud.sleet.fill'), 20: ('Heavy Sleet Shower', 'cloud.sleet.fill'), 21: ('Snow Shower', 'cloud.snow.fill'), 22: ('Heavy Snow Shower', 'cloud.snow.fill'), 23: ('Lightning', 'cloud.bolt.fill'), 24: ('Hail', 'cloud.hail.fill'), 25: ('Thunderstorm', 'cloud.bolt.rain.fill'), 26: ('Heavy Thunderstorm', 'cloud.bolt.rain.fill'), 27: ('Storm', 'wind')} module-attribute #

Each Meteostat condition code as its condition name and SF Symbol.

OKTAS = 8.0 module-attribute #

The full-sky cloud cover Meteostat's cldc counts eighths of.

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

What a condition code outside the table reports.

fetch_past_weather(location, when) #

Fetch the historical weather at a location and time.

Parameters:

Name Type Description Default
location Location

The place to fetch for, including its altitude.

required
when datetime

The moment to fetch for (naive is treated as local).

required

Returns:

Type Description
Weather

A cg.Weather with the available observations; the fields Meteostat

Weather

does not report come back as nan.

Raises:

Type Description
ImportError

If meteostat is not installed.

ValueError

If no observations cover the location and time.

Example
import capturegraph as cg
import capturegraph.scheduling as cgsh
from datetime import datetime

loc = cg.Location(latitude=42.445, longitude=-76.480, altitude_meters=261.5)
weather = cgsh.forecast.fetch_past_weather(loc, datetime(2024, 6, 15, 12, 0))
weather.temperature_celsius  # 22.5
Source code in capturegraph-lib/capturegraph/scheduling/forecast/meteostat.py
def fetch_past_weather(location: cg.Location, when: datetime) -> cg.Weather:
    """Fetch the historical weather at a location and time.

    Args:
        location: The place to fetch for, including its altitude.
        when: The moment to fetch for (naive is treated as local).

    Returns:
        A ``cg.Weather`` with the available observations; the fields Meteostat
        does not report come back as ``nan``.

    Raises:
        ImportError: If meteostat is not installed.
        ValueError: If no observations cover the location and time.

    Example:
        ```python
        import capturegraph as cg
        import capturegraph.scheduling as cgsh
        from datetime import datetime

        loc = cg.Location(latitude=42.445, longitude=-76.480, altitude_meters=261.5)
        weather = cgsh.forecast.fetch_past_weather(loc, datetime(2024, 6, 15, 12, 0))
        weather.temperature_celsius  # 22.5
        ```
    """
    try:
        import meteostat as ms
    except ImportError as error:
        raise ImportError(
            "fetch_past_weather requires meteostat. Install with: uv add meteostat"
        ) from error

    # Meteostat accepts a float elevation in metres; its stub over-narrows to int.
    point = ms.Point(location.latitude, location.longitude, location.altitude_meters)  # pyright: ignore[reportArgumentType]
    stations = ms.stations.nearby(point, limit=4)

    start = when.replace(hour=0, minute=0, second=0, microsecond=0)
    end = when.replace(hour=23, minute=59, second=59, microsecond=0)
    observations = ms.interpolate(ms.hourly(stations, start, end), point).fetch()

    if observations is None or observations.empty:
        raise ValueError(
            f"No weather data available for "
            f"({location.latitude}, {location.longitude}) on {when.date()}"
        )

    target_hour = when.replace(minute=0, second=0, microsecond=0)
    if target_hour in observations.index:
        row = observations.loc[target_hour]
    else:
        row = observations.iloc[observations.index.get_indexer([target_hour], method="nearest")[0]]

    def value(name: str, default: float) -> float:
        raw = row.get(name)
        return default if raw is None or pd.isna(raw) else raw

    temperature = value("temp", 0.0)
    wind_speed = value("wspd", float("nan"))  # km/h
    wind_gust = value("wpgt", float("nan"))  # km/h
    code = value("coco", 1)
    cloud_oktas = value("cldc", float("nan"))
    sunshine_minutes = value("tsun", float("nan"))

    condition, symbol = COCO_CONDITIONS.get(int(code) if code else 1, UNKNOWN_CONDITION)
    if not pd.isna(cloud_oktas):
        cloud_cover_ratio = float(cloud_oktas) / OKTAS
    else:
        cloud_cover_ratio = 0.5 if code and int(code) >= 3 else 0.0
    # bool(...) drops numpy's np.bool_ (tsun is a pandas value), which the
    # Weather codec rejects for a bool field.
    is_daylight = (
        bool(sunshine_minutes > 0) if not pd.isna(sunshine_minutes) else (6 <= when.hour <= 20)
    )

    return cg.Weather(
        temperature_celsius=float(temperature),
        apparent_temperature_celsius=float("nan"),
        dew_point_celsius=float(value("dwpt", temperature)),
        humidity_ratio=float(value("rhum", 50.0)) / 100.0,
        pressure_hpa=float(value("pres", 1013.25)),
        wind_speed_mps=wind_speed / 3.6 if wind_speed else 0.0,
        wind_gust_mps=wind_gust / 3.6 if wind_gust else None,
        wind_direction_degrees=float(value("wdir", 0.0)),
        condition=condition,
        symbol_name=symbol,
        cloud_cover_ratio=cloud_cover_ratio,
        precipitation_intensity_mmph=float(value("prcp", 0.0)),
        visibility_meters=float("nan"),
        uv_index=float("nan"),
        is_daylight=is_daylight,
        time=when,
    )