diff --git a/src/data_flights.py b/src/data_flights.py index 97433e8..fa3680f 100644 --- a/src/data_flights.py +++ b/src/data_flights.py @@ -1,4 +1,4 @@ -"""Flugverkehr-Collector: Globaler Snapshot via OpenSky Network API.""" +"""Flugverkehr: OpenSky (primary) + airplanes.live (fallback).""" import asyncio import logging import time @@ -9,90 +9,109 @@ from fastapi import APIRouter logger = logging.getLogger("globe.flights") 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" +# 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_all(): - """Holt alle Flugzeuge weltweit von OpenSky Network.""" - 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: - 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], ...] +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: + if not s or len(s) < 12 or s[5] is None or s[6] is None: continue - lon = s[5] - lat = s[6] - if lon is None or lat is None: + if s[8]: # on_ground 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, + "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 + + +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"] async def _collector_loop(): - """Background-Loop: Flugdaten alle 15s vorladen.""" - await asyncio.sleep(3) + await asyncio.sleep(5) while True: try: await _fetch_all() except Exception as e: - logger.warning(f"Flight collector error: {e}") - await asyncio.sleep(15) + logger.warning(f"Flight collector: {e}") + await asyncio.sleep(60) 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)") + logger.info("Flight collector gestartet (OpenSky + Fallback)") @router.get("/flights") async def get_flights(): - data = await _fetch_all() - return data + return await _fetch_all()