Save up to 17% with yearly billing

See pricing

Am Flughafen

Lauffähige Beispiele für Wetter am Flughafen: METAR und TAF am Abflug-, Ziel- und Ausweichflughafen. Für bbox-Abfragen auf der Strecke (Höhenwinde, PIREPs, AIRMETs) siehe Auf der Strecke.

#FrageVerwendete Daten
1Welche Flugwetterkategorie gilt gerade jetzt an großen US-Flughäfen?METAR
2Übersteigt die Dichtehöhe die Pistenhöhe um mehr als Ihren Schwellenwert?METAR
3Wie wird das Wetter bei meinem Abflug sein (in den nächsten Stunden)?TAF
4New York → Los Angeles — Wetter bei Start und Landung, aktuell vs. Vorhersage?METAR + TAF
5Aktuelle METAR-Übersicht für ausgewählte internationale FlughäfenMETAR
6Erfüllt das aktuelle METAR eine eigene Checkliste für Wind, Sicht und Wolkenuntergrenze?METAR

Voraussetzungen

Auth
x-api-key bei jeder Anfrage (Direktabonnement). Mehr erfahren →

Installation

Die folgenden Skripte verwenden requests für API-Aufrufe, rich für Terminal-Tabellen sowie plotly + pandas für die Karte der Flugwetterkategorien. Einmalig installieren:

pip install requests rich plotly pandas

1. Karte der aktuellen Flugwetterkategorien (Live-METAR)

Frage: Welche aktuelle Flugwetterkategorie ergibt sich an vier großen US-Flughäfen aus dem METAR?

METAR liefert eine Flugwetterkategorie (VFR / MVFR / IFR / LIFR). Dieses Skript stellt jeden Flughafen auf einer US-Karte dar, mit farbcodierten Beschriftungen gemäß der Tabelle der Flugwetterkategorien.

import os
import webbrowser
from pathlib import Path

import pandas as pd
import plotly.express as px
import requests

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

# Demo airports: code → (latitude, longitude, city name)
AIRPORTS = {
    "KJFK": (40.6413, -73.7781, "New York (JFK)"),
    "KLAX": (33.9425, -118.4081, "Los Angeles"),
    "KORD": (41.9742, -87.9073, "Chicago"),
    "KDEN": (39.8561, -104.6737, "Denver"),
}

# Flight category → color + label (see METAR reference)
CONDITIONS = {
    "VFR":  ("#22c55e", "VFR - visual flight rules"),
    "MVFR": ("#3b82f6", "MVFR - marginal VFR"),
    "IFR":  ("#ef4444", "IFR - instrument flight rules"),
    "LIFR": ("#a855f7", "LIFR - low IFR"),
}


def fetch_metar(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def build_map() -> Path:
    rows = []
    for icao, (lat, lon, city) in AIRPORTS.items():
        metar = fetch_metar(icao)
        if not metar:
            continue
        p = metar["parsed"]
        code = p["flight_rules"]
        _color, label = CONDITIONS.get(code, ("#6b7280", code))
        wind = p["wind"]["speed"]
        rows.append({
            "city": city,
            "code": icao,
            "lat": lat,
            "lon": lon,
            "rating_code": code,
            "conditions": label,
            "wind": f"{wind} kt (~{round(wind * 1.15)} mph, ~{round(wind * 1.852)} km/h)",
            "map_label": f"{city}: {label}",
        })

    df = pd.DataFrame(rows)
    color_map = {k: v[0] for k, v in CONDITIONS.items()}

    fig = px.scatter_geo(
        df,
        lat="lat",
        lon="lon",
        color="rating_code",
        color_discrete_map=color_map,
        hover_name="city",
        hover_data={
            "conditions": True,
            "wind": True,
            "code": True,
            "rating_code": False,
            "lat": False,
            "lon": False,
            "map_label": False,
        },
        scope="usa",
        title="Current flight category by airport (METAR)",
        labels={"rating_code": "API code"},
    )
    fig.update_traces(
        marker={"size": 18, "line": {"width": 2, "color": "white"}},
        text=df["map_label"],
        textposition="top center",
    )
    fig.add_annotation(
        text="Green = VFR  |  Blue = MVFR  |  Red/magenta = IFR/LIFR",
        xref="paper", yref="paper", x=0.5, y=-0.08, showarrow=False,
        font={"size": 12},
    )

    out = Path("weather_map.html")
    fig.write_html(out, include_plotlyjs="cdn")
    return out


if __name__ == "__main__":
    path = build_map()
    print(f"Saved {path} - opening interactive map in your browser.")
    print("Hover a marker for wind and category. Colors follow METAR flight_rules.")
    webbrowser.open(path.resolve().as_uri())

Ausgabe: HTML-Karte mit vier US-Flughäfen, eingefärbt nach flight_rules, mit Hover-Daten zu Wind und Kategoriebezeichnung.


2. Dichtehöhe im Vergleich zur Pistenhöhe

Frage: Übersteigt parsed.density_altitude an diesem Flughafen die Pistenhöhe um mehr als Ihren Schwellenwert?

Das METAR enthält die Dichtehöhe (wie dünn sich die Luft anfühlt). Vergleichen Sie sie mit der Pistenhöhe: Ein großer Abstand bedeutet eine längere Startrollstrecke.

import os

import requests
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"


def fetch_metar(icao: str) -> dict:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    r.raise_for_status()
    return r.json()


def takeoff_performance_report(
    icao: str,
    runway_elevation_ft: int,
    concern_if_da_above_runway_ft: int = 1000,
) -> None:
    metar = fetch_metar(icao)
    name = metar.get("airport_name", icao)
    p = metar["parsed"]
    da = p["density_altitude"]
    gap = da - runway_elevation_ft
    hot_day = gap >= concern_if_da_above_runway_ft

    table = Table(title=f"Takeoff air density - {name} ({icao})")
    table.add_column("What", style="cyan")
    table.add_column("Value", justify="right")
    table.add_row("Runway elevation", f"{runway_elevation_ft} ft above sea level")
    table.add_row("Air feels like elevation", f"{da} ft (density altitude)")
    table.add_row("Difference", f"+{gap} ft - air is thinner than runway height suggests")
    table.add_row("Outside temperature", f"{p['temperature']} C / {round(p['temperature'] * 9/5 + 32)} F")
    table.add_row("Barometer", f"{p['altimeter']} inHg")
    console.print(table)

    if hot_day:
        msg = (
            f"The air is significantly thinner than normal for this runway ({gap} ft gap). "
            "Planes need more runway to lift off and climb slower. "
            "Check your aircraft handbook before assuming normal takeoff distance."
        )
        console.print(Panel(msg, title="WARN: possible reduced takeoff performance", style="red"))
    else:
        msg = (
            f"Density altitude is within {concern_if_da_above_runway_ft} ft of runway elevation. "
            "Takeoff performance is likely close to normal (still verify for your aircraft)."
        )
        console.print(Panel(msg, title="OK: no major thin-air warning for this threshold", style="green"))


if __name__ == "__main__":
    # JFK runway is almost at sea level (~13 ft)
    takeoff_performance_report("KJFK", runway_elevation_ft=13)

Ausgabe: Tabelle, die Pistenhöhe, Dichtehöhe und Temperatur gegenüberstellt; ein Panel schlägt an, wenn der Abstand den Schwellenwert überschreitet.


3. Wie wird das Wetter bei meinem Abflug sein?

Frage: Ich plane, zur aktuellen UTC-Zeit in Chicago zu starten — mit welchen Bedingungen muss ich rechnen?

Der TAF ist eine Zeitleiste aus Vorhersageblöcken. Am wahrscheinlichsten = der Haupt-FROM-Block; mögliche kurze Phasen = überlappende TEMPO- / BECMG-Zeilen.

import os
from datetime import datetime, timezone

import requests
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

CONDITIONS = {
    "VFR":  "VFR - visual flight rules",
    "MVFR": "MVFR - marginal VFR",
    "IFR":  "IFR - instrument flight rules",
    "LIFR": "LIFR - low IFR",
}

LAYER_MEANING = {
    "FROM":  "Most likely for this time window",
    "TEMPO": "Possible brief worse spell (may not happen)",
    "BECMG": "Conditions gradually changing toward",
}


def parse_time(iso: str) -> datetime:
    return datetime.fromisoformat(iso.replace("Z", "+00:00"))


def fetch_taf(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/taf/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def print_departure_forecast(icao: str, departure: datetime) -> None:
    taf = fetch_taf(icao)
    if not taf:
        console.print(f"[red]No forecast (TAF) published for {icao} - common at small airfields.[/]")
        return

    name = taf.get("airport_name", icao)
    console.print(Panel(
        f"Forecast airport: {name} ({icao})\n"
        f"Departure time (UTC): {departure.strftime('%Y-%m-%d %H:%M')} Z\n"
        f"Below = parsed TAF layers active at that instant.",
        title="Question: forecast at departure time",
    ))

    forecast = taf["parsed"]["forecast"]
    baseline = next(
        (
            p for p in forecast
            if p["type"] == "FROM"
            and parse_time(p["start_time"]) <= departure <= parse_time(p["end_time"])
        ),
        None,
    )
    modifiers = [
        p for p in forecast
        if p is not baseline
        and parse_time(p["start_time"]) <= departure <= parse_time(p["end_time"])
    ]

    table = Table(title="Forecast layers at your departure time")
    table.add_column("Role", style="cyan")
    table.add_column("Conditions")
    table.add_column("Weather details")

    if baseline:
        code = baseline["flight_rules"]
        wx = ", ".join(w["repr"] for w in baseline["wx_codes"]) or "No significant weather"
        table.add_row(
            LAYER_MEANING.get("FROM", "Most likely"),
            CONDITIONS.get(code, code),
            wx,
        )
    else:
        table.add_row("Most likely", "-", "No matching forecast block for this time")

    for p in modifiers:
        code = p["flight_rules"]
        wx = ", ".join(w["repr"] for w in p["wx_codes"]) or "-"
        prob = (p.get("probability") or {}).get("value")
        role = LAYER_MEANING.get(p["type"], p["type"])
        if prob:
            role += f" ({prob}% chance)"
        table.add_row(role, CONDITIONS.get(code, code), wx)

    console.print(table)
    console.print(
        "\n[dim]Show the baseline FROM row and any overlapping TEMPO/BECMG rows separately.[/dim]"
    )


if __name__ == "__main__":
    print_departure_forecast("KORD", datetime.now(timezone.utc))

Ausgabe: Panel mit der Abflugzeit sowie eine Tabelle mit der aktiven FROM-Zeile und den überlappenden Modifikator-Perioden.


4. New York → Los Angeles — Wetter an beiden Enden?

Frage: Ich fliege KJFK → KLAX — wie ist das Wetter gerade jetzt am Abflug- und am Zielflughafen, und was ist die ungünstigste Vorhersage für die nächsten ~24 Stunden?

import os
import concurrent.futures

import requests
from rich.console import Console
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

CONDITIONS = {
    "VFR":  "VFR - visual flight rules",
    "MVFR": "MVFR - marginal VFR",
    "IFR":  "IFR - instrument flight rules",
    "LIFR": "LIFR - low IFR",
}
SEVERITY = {"VFR": 0, "MVFR": 1, "IFR": 2, "LIFR": 3}
STYLE = {"VFR": "green", "MVFR": "blue", "IFR": "red", "LIFR": "magenta"}


def fetch_metar(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def fetch_taf(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/taf/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def worst_forecast_label(taf: dict | None) -> str:
    if not taf:
        return "No forecast available"
    from_blocks = [p for p in taf["parsed"]["forecast"] if p["type"] == "FROM"]
    if not from_blocks:
        return "No forecast available"
    worst = max(from_blocks, key=lambda p: SEVERITY.get(p["flight_rules"], -1))
    code = worst["flight_rules"]
    return CONDITIONS.get(code, code)


def print_route_weather(dep_icao: str, arr_icao: str) -> None:
    with concurrent.futures.ThreadPoolExecutor() as ex:
        metar_dep = ex.submit(fetch_metar, dep_icao).result()
        taf_dep = ex.submit(fetch_taf, dep_icao).result()
        metar_arr = ex.submit(fetch_metar, arr_icao).result()
        taf_arr = ex.submit(fetch_taf, arr_icao).result()

    table = Table(title=f"Route weather: {dep_icao} (departure) -> {arr_icao} (arrival)")
    table.add_column("Airport", style="cyan")
    table.add_column("Weather RIGHT NOW", max_width=36)
    table.add_column("Worst forecast in next ~24 h", max_width=36)

    for icao, metar, taf in [
        (dep_icao, metar_dep, taf_dep),
        (arr_icao, metar_arr, taf_arr),
    ]:
        if metar:
            code = metar["parsed"]["flight_rules"]
            now = CONDITIONS.get(code, code)
            style = STYLE.get(code, "white")
            airport = metar.get("airport_name", icao)
        else:
            now, style, airport = "No observation", "white", icao

        table.add_row(
            f"{airport}\n({icao})",
            f"[{style}]{now}[/]",
            worst_forecast_label(taf),
        )

    console.print(table)
    console.print(
        "\n[dim]RIGHT NOW = live METAR observation. "
        "Worst forecast = poorest 'most likely' TAF block in the next day.[/dim]"
    )


if __name__ == "__main__":
    print_route_weather("KJFK", "KLAX")

Ausgabe: Zwei Zeilen (Abflug und Ankunft): aktuelle flight_rules aus dem METAR und die ungünstigste FROM-Kategorie aus dem TAF.


5. METAR-Dashboard für mehrere Flughäfen

Frage: Dekodierte METAR-Daten für eine feste Liste von Flughäfen abrufen und Wind, Temperatur, Sicht und flight_rules ausgeben.

import os

import requests
from rich.console import Console
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

AIRPORTS = ["KJFK", "KLAX", "EGLL", "YSSY"]  # New York, LA, London, Sydney

CONDITIONS = {
    "VFR":  "VFR - visual flight rules",
    "MVFR": "MVFR - marginal VFR",
    "IFR":  "IFR - instrument flight rules",
    "LIFR": "LIFR - low IFR",
}
STYLE = {"VFR": "green", "MVFR": "blue", "IFR": "red", "LIFR": "magenta"}


def fetch_metar(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def visibility_text(parsed: dict) -> str:
    v = parsed["visibility"]["value"]
    repr_ = parsed["visibility"]["repr"]
    if v is None:
        return "More than 6 miles visibility"
    # Some international stations return coded values (e.g. 9999 = 10 km+) - not statute miles.
    if v > 50 or repr_ in ("9999", "P6"):
        return f"{repr_} (coded - check raw METAR for units)"
    return f"{v} miles visibility"


def print_live_dashboard(airports: list[str]) -> None:
    table = Table(title="Live airport weather (METAR - updates every 30–60 min)")
    table.add_column("Airport", style="cyan", max_width=28)
    table.add_column("Flight category", max_width=30)
    table.add_column("Wind")
    table.add_column("Temp")
    table.add_column("Visibility")

    for icao in airports:
        data = fetch_metar(icao)
        if not data:
            table.add_row(icao, "No data", "-", "-", "-")
            continue
        p = data["parsed"]
        code = p["flight_rules"]
        label = CONDITIONS.get(code, code)
        style = STYLE.get(code, "white")
        gust = p["wind"]["gust"]
        wind = f"{p['wind']['speed']} kt"
        if gust:
            wind += f" (gusts {gust} kt)"
        table.add_row(
            data["airport_name"],
            f"[{style}]{label}[/]",
            wind,
            f"{p['temperature']} C",
            visibility_text(p),
        )

    console.print(table)
    console.print(
        "\n[dim]In a real dashboard, refresh every 5 minutes - "
        "that is often enough and saves API quota.[/dim]"
    )


if __name__ == "__main__":
    print_live_dashboard(AIRPORTS)

Ausgabe: Terminal-Tabelle mit Flughafenname, flight_rules-Bezeichnung, Wind, Temperatur und Sicht.


6. Checkliste mit persönlichen Limits (Demo)

Frage: Erfüllt das aktuelle METAR konfigurierbare Limits für Wind, Böen, Sicht und Wolkenuntergrenze?

Kein Ersatz für ein offizielles Wetterbriefing oder behördlich vorgeschriebene Mindestwerte.

import os

import requests
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

# Demo personal limits - change these to match your skills / aircraft
MY_LIMITS = {
    "max_wind_kt": 25,
    "max_gust_kt": 35,
    "min_visibility_sm": 1.0,
    "min_ceiling_ft": 500,
}


def fetch_metar(icao: str) -> dict:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    r.raise_for_status()
    return r.json()


def print_safety_check(icao: str) -> None:
    metar = fetch_metar(icao)
    name = metar.get("airport_name", icao)
    p = metar["parsed"]
    code = p["flight_rules"]

    limits = Table(title=f"Your demo limits (edit MY_LIMITS in the script)")
    limits.add_column("Rule")
    limits.add_column("Your limit", justify="right")
    limits.add_row("Max steady wind", f"{MY_LIMITS['max_wind_kt']} kt")
    limits.add_row("Max gusts", f"{MY_LIMITS['max_gust_kt']} kt")
    limits.add_row("Min visibility", f"{MY_LIMITS['min_visibility_sm']} miles")
    limits.add_row("Min cloud ceiling", f"{MY_LIMITS['min_ceiling_ft']} ft")
    console.print(limits)

    problems: list[str] = []

    wind = p["wind"]["speed"]
    if wind > MY_LIMITS["max_wind_kt"]:
        problems.append(f"Wind too strong: {wind} kt (your max {MY_LIMITS['max_wind_kt']} kt)")

    gust = p["wind"]["gust"]
    if gust and gust > MY_LIMITS["max_gust_kt"]:
        problems.append(f"Gusts too strong: {gust} kt (your max {MY_LIMITS['max_gust_kt']} kt)")

    vis = p["visibility"]["value"]
    if vis is not None and vis < MY_LIMITS["min_visibility_sm"]:
        problems.append(
            f"Visibility too low: {vis} miles (you need at least {MY_LIMITS['min_visibility_sm']} mi)"
        )

    ceiling_layers = [c for c in p["clouds"] if c["type"] in ("BKN", "OVC")]
    if ceiling_layers:
        ceiling_ft = ceiling_layers[0]["base"] * 100
        if ceiling_ft < MY_LIMITS["min_ceiling_ft"]:
            problems.append(
                f"Clouds too low: base at {ceiling_ft} ft (you need at least {MY_LIMITS['min_ceiling_ft']} ft)"
            )

    safe = len(problems) == 0
    if safe:
        body = (
            f"All demo checks passed at {name}.\n"
            f"Current overall rating from METAR: {code}.\n"
            "This does NOT replace a full weather briefing."
        )
        title = f"Within demo limits - {icao}"
        style = "green"
    else:
        body = "Problems found:\n" + "\n".join(f"- {x}" for x in problems)
        title = f"Exceeds demo limits - {icao}"
        style = "red"

    console.print(Panel(body, title=title, style=style))


if __name__ == "__main__":
    print_safety_check("KJFK")

Ausgabe: Tabelle mit den Limits, danach ein Panel, das bestandene Prüfungen oder jede verletzte Regel gemessen an MY_LIMITS auflistet.


Wetter auf der Strecke (bbox-Endpoints)

Die Fragen 7–9 fragen einen Begrenzungsrahmen (Bounding Box) entlang Ihrer Route ab. Bilden Sie den Rahmen aus den Koordinaten von Abflug und Ankunft (plus Puffer) oder aus Wegpunkten. Ein leeres reports / total: 0 ist normal, wenn keine PIREPs abgegeben wurden oder keine Warnmeldungen aktiv sind — kein API-Fehler.

Im Folgenden verwendete Hilfsfunktion:

def route_bbox(
    dep_lat: float, dep_lon: float,
    arr_lat: float, arr_lon: float,
    pad_deg: float = 2.0,
) -> str:
    """SW corner first, then NE - matches API bbox format."""
    lat1 = min(dep_lat, arr_lat) - pad_deg
    lat2 = max(dep_lat, arr_lat) + pad_deg
    lon1 = min(dep_lon, arr_lon) - pad_deg
    lon2 = max(dep_lon, arr_lon) + pad_deg
    return f"{lat1},{lon1},{lat2},{lon2}"

Koordinaten für KJFK (40.6413, -73.7781) und KLAX (33.9425, -118.4081).