Flights: Umstellung auf OpenSky Network API

airplanes.live ersetzt durch OpenSky Network (opensky-network.org).
Ein einzelner API-Call liefert ALLE Flugzeuge weltweit (~6.500).
Kein Grid-System mehr, kein Rate-Limiting, keine fehlenden Regionen.
Refresh alle 15s. Am Boden stehende Flugzeuge gefiltert.

Regionale Abdeckung verifiziert: Europa 2.662, Nordamerika 2.220,
Asien 688, Suedamerika 221, Afrika 92, Nahost 48, Russland 42.
Dieser Commit ist enthalten in:
Claude Dev
2026-03-24 12:27:31 +01:00
Ursprung 6f4c5ab3a6
Commit edff097868

Datei anzeigen

@@ -1,4 +1,4 @@
"""Flugverkehr-Collector: Globaler Snapshot via airplanes.live.""" """Flugverkehr-Collector: Globaler Snapshot via OpenSky Network API."""
import asyncio import asyncio
import logging import logging
import time import time
@@ -9,82 +9,87 @@ from fastapi import APIRouter
logger = logging.getLogger("globe.flights") logger = logging.getLogger("globe.flights")
router = APIRouter() router = APIRouter()
# 64 Stuetzpunkte fuer globale Abdeckung (je 250nm Radius) # OpenSky Network: Ein Call = alle Flugzeuge weltweit
_GRID = [ # Anonym: max 1 Call / 10 Sekunden, ~7000+ Flugzeuge
(48,2),(48,16),(55,10),(40,-4),(41,12),(38,24),(55,25),(60,25),(52,30),(45,37), _OPENSKY_URL = "https://opensky-network.org/api/states/all"
(54,-2),(63,-19),
(33,36),(30,31),(25,45),(26.5,56),(25,51.5),(33,44),(33,52),(15,45),(21,40),
(34,2),(33,-7),(32,13),(41,69),(39,63),
(40,-74),(33,-84),(42,-88),(26,-80),(45,-74),(34,-118),(47,-122),(37,-122),(30,-97),(39,-105),
(35,140),(37,127),(31,121),(40,117),(22,114),(25,121),
(19,73),(28,77),(13,80),(7,80),(1,104),(14,101),(-6,107),(10,107),
(-34,151),(-37,175),(-1,37),(-34,18),(6,3),(9,39),
(-23,-43),(-34,-58),(-12,-77),(4,-74),
]
_cache: dict = {"data": None, "ts": 0} _cache: dict = {"data": None, "ts": 0}
_lock = asyncio.Lock() _lock = asyncio.Lock()
_task = None _task = None
import random
async def _fetch_all(): async def _fetch_all():
"""Holt Flugdaten fuer alle Stuetzpunkte.""" """Holt alle Flugzeuge weltweit von OpenSky Network."""
now = time.time() now = time.time()
if _cache["data"] and now - _cache["ts"] < 170: if _cache["data"] and now - _cache["ts"] < 14:
return _cache["data"] return _cache["data"]
async with _lock: async with _lock:
if _cache["data"] and time.time() - _cache["ts"] < 25: if _cache["data"] and time.time() - _cache["ts"] < 14:
return _cache["data"] return _cache["data"]
seen = {}
errors = 0
grid = list(_GRID)
random.shuffle(grid)
async with httpx.AsyncClient(timeout=10) as client:
for i in range(0, len(grid), 3):
batch = grid[i:i+3]
tasks = [client.get(f"https://api.airplanes.live/v2/point/{lat:.2f}/{lon:.2f}/250")
for lat, lon in batch]
results = await asyncio.gather(*tasks, return_exceptions=True)
for r in results:
if isinstance(r, Exception):
errors += 1
continue
try: try:
for ac in r.json().get("ac", []): async with httpx.AsyncClient(timeout=20) as client:
h = ac.get("hex") resp = await client.get(_OPENSKY_URL)
if h and h not in seen: resp.raise_for_status()
seen[h] = ac raw = resp.json()
except Exception: except Exception as e:
errors += 1 logger.warning(f"OpenSky Fehler: {e}")
if i + 3 < len(grid): return _cache["data"] or {"ac": [], "total": 0}
await asyncio.sleep(5.0)
_cache["data"] = {"ac": list(seen.values()), "total": len(seen), "errors": errors} # 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", [])
ac = []
for s in states:
if not s or len(s) < 12:
continue
lon = s[5]
lat = s[6]
if lon is None or lat is None:
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({
"hex": icao,
"flight": callsign,
"lat": lat,
"lon": lon,
"alt_baro": round(alt * 3.281) if alt else None, # m -> ft
"gs": round(velocity * 1.944) if velocity else None, # m/s -> kts
"track": heading,
"origin": origin,
})
_cache["data"] = {"ac": ac, "total": len(ac)}
_cache["ts"] = time.time() _cache["ts"] = time.time()
logger.info(f"Flights: {len(seen)} Flugzeuge ({errors} Fehler)") 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 30s vorladen.""" """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 error: {e}")
await asyncio.sleep(180) await asyncio.sleep(15)
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") logger.info("Flight collector gestartet (OpenSky)")
@router.get("/flights") @router.get("/flights")