Flugverkehr: adsb.lol als neue Primaerquelle (10.796 Flugzeuge)

OpenSky + airplanes.live ersetzt durch adsb.lol:
- Ein Call: 10.796 Flugzeuge global
- Kein Rate-Limiting
- 15s Refresh, 12s Cache
- Fallback: adsb.one (~7.000 Flugzeuge)
- Mehr Datenfelder: Registration, Flugzeugtyp, Squawk
Dieser Commit ist enthalten in:
Claude Dev
2026-03-24 14:55:30 +01:00
Ursprung a071fb13bd
Commit d3a45f1901

Datei anzeigen

@@ -1,4 +1,4 @@
"""Flugverkehr: OpenSky (primary) + airplanes.live (fallback)."""
"""Flugverkehr: adsb.lol (primary, 10.000+) + adsb.one (fallback)."""
import asyncio
import logging
import time
@@ -9,82 +9,71 @@ from fastapi import APIRouter
logger = logging.getLogger("globe.flights")
router = APIRouter()
_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}
_lock = asyncio.Lock()
_task = None
async def _fetch_opensky(client):
"""Versucht OpenSky — liefert alle Flugzeuge oder None bei Fehler."""
try:
resp = await client.get(_OPENSKY_URL)
if resp.status_code == 429:
logger.info("OpenSky: Rate-Limited (429), nutze Fallback")
return None
resp.raise_for_status()
raw = resp.json()
states = raw.get("states", [])
ac = []
for s in states:
if not s or len(s) < 12 or s[5] is None or s[6] is None:
continue
if s[8]: # on_ground
continue
ac.append({
"hex": s[0] or "", "flight": (s[1] or "").strip(),
"lat": s[6], "lon": s[5],
"alt_baro": round(s[7] * 3.281) if s[7] else None,
"gs": round(s[9] * 1.944) if s[9] else None,
"track": s[10], "origin": s[2] or "",
})
logger.info(f"OpenSky: {len(ac)} Flugzeuge")
return ac
except Exception as e:
logger.warning(f"OpenSky: {e}")
return None
# adsb.lol: Ein Call, 10.000+ Flugzeuge, kein Rate-Limit
_PRIMARY_URL = "https://api.adsb.lol/v2/point/30/0/10000"
# adsb.one: Fallback, ~7.000 Flugzeuge
_FALLBACK_URL = "https://api.adsb.one/v2/point/30/0/10000"
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())
def _parse_ac(data):
"""Parst Flugzeuge aus adsb.lol/adsb.one Format."""
ac = []
for a in data.get("ac", []):
lat = a.get("lat")
lon = a.get("lon")
if lat is None or lon is None:
continue
ac.append({
"hex": a.get("hex", ""),
"flight": (a.get("flight") or "").strip(),
"reg": a.get("r", ""),
"type": a.get("t", ""),
"lat": lat,
"lon": lon,
"alt_baro": a.get("alt_baro"),
"alt_geom": a.get("alt_geom"),
"gs": a.get("gs"),
"track": a.get("track"),
"squawk": a.get("squawk", ""),
})
return ac
async def _fetch_all():
now = time.time()
if _cache["data"] and now - _cache["ts"] < 55:
if _cache["data"] and now - _cache["ts"] < 12:
return _cache["data"]
async with _lock:
if _cache["data"] and time.time() - _cache["ts"] < 55:
if _cache["data"] and time.time() - _cache["ts"] < 12:
return _cache["data"]
ac = None
async with httpx.AsyncClient(timeout=20) as client:
ac = await _fetch_opensky(client)
if ac is None:
ac = await _fetch_fallback(client)
# Primary: adsb.lol
try:
resp = await client.get(_PRIMARY_URL)
if resp.status_code == 200:
ac = _parse_ac(resp.json())
if ac:
logger.info(f"adsb.lol: {len(ac)} Flugzeuge")
except Exception as e:
logger.warning(f"adsb.lol: {e}")
# Fallback: adsb.one
if not ac:
try:
resp = await client.get(_FALLBACK_URL)
if resp.status_code == 200:
ac = _parse_ac(resp.json())
if ac:
logger.info(f"adsb.one Fallback: {len(ac)} Flugzeuge")
except Exception as e:
logger.warning(f"adsb.one: {e}")
if ac:
_cache["data"] = {"ac": ac, "total": len(ac)}
@@ -96,20 +85,20 @@ async def _fetch_all():
async def _collector_loop():
await asyncio.sleep(5)
await asyncio.sleep(3)
while True:
try:
await _fetch_all()
except Exception as e:
logger.warning(f"Flight collector: {e}")
await asyncio.sleep(60)
await asyncio.sleep(15)
def start_flight_collector():
global _task
if _task is None or _task.done():
_task = asyncio.create_task(_collector_loop())
logger.info("Flight collector gestartet (OpenSky + Fallback)")
logger.info("Flight collector gestartet (adsb.lol + adsb.one)")
@router.get("/flights")