Flugverkehr: Dual-Source (OpenSky + airplanes.live Fallback)

OpenSky als Primary (alle Flugzeuge weltweit, ein Call).
Bei 429 Rate-Limit: automatischer Fallback auf airplanes.live
mit 14 Stuetzpunkten (Europa, USA, Nahost, Asien).
Cache 55s, Collector-Interval 60s.
965+ Flugzeuge auch bei OpenSky-Sperre.
Dieser Commit ist enthalten in:
Claude Dev
2026-03-24 14:51:52 +01:00
Ursprung 01be1d5a81
Commit a071fb13bd

Datei anzeigen

@@ -1,4 +1,4 @@
"""Flugverkehr-Collector: Globaler Snapshot via OpenSky Network API.""" """Flugverkehr: OpenSky (primary) + airplanes.live (fallback)."""
import asyncio import asyncio
import logging import logging
import time import time
@@ -9,90 +9,109 @@ from fastapi import APIRouter
logger = logging.getLogger("globe.flights") logger = logging.getLogger("globe.flights")
router = APIRouter() router = APIRouter()
# OpenSky Network: Ein Call = alle Flugzeuge weltweit
# Anonym: max 1 Call / 10 Sekunden, ~7000+ Flugzeuge
_OPENSKY_URL = "https://opensky-network.org/api/states/all" _OPENSKY_URL = "https://opensky-network.org/api/states/all"
# Fallback: airplanes.live Stuetzpunkte (wenige, wichtigste)
_FALLBACK_GRID = [
(48, 10), (52, 13), (40, -4), (41, 12), (55, 10), # Europa
(40, -74), (34, -118), (42, -88), # USA
(33, 36), (26, 56), (25, 45), # Nahost
(35, 140), (31, 121), (22, 114), # Asien
]
_cache: dict = {"data": None, "ts": 0} _cache: dict = {"data": None, "ts": 0}
_lock = asyncio.Lock() _lock = asyncio.Lock()
_task = None _task = None
async def _fetch_all(): async def _fetch_opensky(client):
"""Holt alle Flugzeuge weltweit von OpenSky Network.""" """Versucht OpenSky — liefert alle Flugzeuge oder None bei Fehler."""
now = time.time() try:
if _cache["data"] and now - _cache["ts"] < 14: resp = await client.get(_OPENSKY_URL)
return _cache["data"] if resp.status_code == 429:
logger.info("OpenSky: Rate-Limited (429), nutze Fallback")
async with _lock: return None
if _cache["data"] and time.time() - _cache["ts"] < 14: resp.raise_for_status()
return _cache["data"] raw = resp.json()
try:
async with httpx.AsyncClient(timeout=20) as client:
resp = await client.get(_OPENSKY_URL)
resp.raise_for_status()
raw = resp.json()
except Exception as e:
logger.warning(f"OpenSky Fehler: {e}")
return _cache["data"] or {"ac": [], "total": 0}
# OpenSky Format: states = [[icao24, callsign, origin, time_pos, last_contact, lon, lat, baro_alt, on_ground, velocity, heading, vert_rate, sensors, geo_alt, squawk, spi, pos_source], ...]
states = raw.get("states", []) states = raw.get("states", [])
ac = [] ac = []
for s in states: for s in states:
if not s or len(s) < 12: if not s or len(s) < 12 or s[5] is None or s[6] is None:
continue continue
lon = s[5] if s[8]: # on_ground
lat = s[6]
if lon is None or lat is None:
continue continue
alt = s[7] # baro altitude in meters
on_ground = s[8]
if on_ground:
continue # Am Boden stehende Flugzeuge ausblenden
velocity = s[9] # m/s
heading = s[10]
callsign = (s[1] or "").strip()
icao = s[0] or ""
origin = s[2] or ""
ac.append({ ac.append({
"hex": icao, "hex": s[0] or "", "flight": (s[1] or "").strip(),
"flight": callsign, "lat": s[6], "lon": s[5],
"lat": lat, "alt_baro": round(s[7] * 3.281) if s[7] else None,
"lon": lon, "gs": round(s[9] * 1.944) if s[9] else None,
"alt_baro": round(alt * 3.281) if alt else None, # m -> ft "track": s[10], "origin": s[2] or "",
"gs": round(velocity * 1.944) if velocity else None, # m/s -> kts
"track": heading,
"origin": origin,
}) })
logger.info(f"OpenSky: {len(ac)} Flugzeuge")
return ac
except Exception as e:
logger.warning(f"OpenSky: {e}")
return None
async def _fetch_fallback(client):
"""Fallback: airplanes.live mit wenigen Stuetzpunkten."""
seen = {}
for lat, lon in _FALLBACK_GRID:
try:
resp = await client.get(f"https://api.airplanes.live/v2/point/{lat:.0f}/{lon:.0f}/250")
if resp.status_code == 200:
for a in resp.json().get("ac", []):
h = a.get("hex")
if h and h not in seen:
seen[h] = a
except Exception:
pass
await asyncio.sleep(2) # Langsam um Rate-Limits zu vermeiden
logger.info(f"airplanes.live Fallback: {len(seen)} Flugzeuge")
return list(seen.values())
async def _fetch_all():
now = time.time()
if _cache["data"] and now - _cache["ts"] < 55:
return _cache["data"]
async with _lock:
if _cache["data"] and time.time() - _cache["ts"] < 55:
return _cache["data"]
async with httpx.AsyncClient(timeout=20) as client:
ac = await _fetch_opensky(client)
if ac is None:
ac = await _fetch_fallback(client)
if ac:
_cache["data"] = {"ac": ac, "total": len(ac)}
_cache["ts"] = time.time()
elif not _cache["data"]:
_cache["data"] = {"ac": [], "total": 0}
_cache["data"] = {"ac": ac, "total": len(ac)}
_cache["ts"] = time.time()
logger.info(f"Flights: {len(ac)} Flugzeuge (OpenSky)")
return _cache["data"] return _cache["data"]
async def _collector_loop(): async def _collector_loop():
"""Background-Loop: Flugdaten alle 15s vorladen.""" await asyncio.sleep(5)
await asyncio.sleep(3)
while True: while True:
try: try:
await _fetch_all() await _fetch_all()
except Exception as e: except Exception as e:
logger.warning(f"Flight collector error: {e}") logger.warning(f"Flight collector: {e}")
await asyncio.sleep(15) await asyncio.sleep(60)
def start_flight_collector(): def start_flight_collector():
global _task global _task
if _task is None or _task.done(): if _task is None or _task.done():
_task = asyncio.create_task(_collector_loop()) _task = asyncio.create_task(_collector_loop())
logger.info("Flight collector gestartet (OpenSky)") logger.info("Flight collector gestartet (OpenSky + Fallback)")
@router.get("/flights") @router.get("/flights")
async def get_flights(): async def get_flights():
data = await _fetch_all() return await _fetch_all()
return data