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:
@@ -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()
|
|
||||||
if _cache["data"] and now - _cache["ts"] < 14:
|
|
||||||
return _cache["data"]
|
|
||||||
|
|
||||||
async with _lock:
|
|
||||||
if _cache["data"] and time.time() - _cache["ts"] < 14:
|
|
||||||
return _cache["data"]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=20) as client:
|
|
||||||
resp = await client.get(_OPENSKY_URL)
|
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()
|
resp.raise_for_status()
|
||||||
raw = resp.json()
|
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["data"] = {"ac": ac, "total": len(ac)}
|
||||||
_cache["ts"] = time.time()
|
_cache["ts"] = time.time()
|
||||||
logger.info(f"Flights: {len(ac)} Flugzeuge (OpenSky)")
|
elif not _cache["data"]:
|
||||||
|
_cache["data"] = {"ac": [], "total": 0}
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren