diff --git a/src/data_flights.py b/src/data_flights.py index c10f315..97433e8 100644 --- a/src/data_flights.py +++ b/src/data_flights.py @@ -1,4 +1,4 @@ -"""Flugverkehr-Collector: Globaler Snapshot via airplanes.live.""" +"""Flugverkehr-Collector: Globaler Snapshot via OpenSky Network API.""" import asyncio import logging import time @@ -9,82 +9,87 @@ from fastapi import APIRouter logger = logging.getLogger("globe.flights") router = APIRouter() -# 64 Stuetzpunkte fuer globale Abdeckung (je 250nm Radius) -_GRID = [ - (48,2),(48,16),(55,10),(40,-4),(41,12),(38,24),(55,25),(60,25),(52,30),(45,37), - (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), -] +# OpenSky Network: Ein Call = alle Flugzeuge weltweit +# Anonym: max 1 Call / 10 Sekunden, ~7000+ Flugzeuge +_OPENSKY_URL = "https://opensky-network.org/api/states/all" _cache: dict = {"data": None, "ts": 0} _lock = asyncio.Lock() _task = None -import random - async def _fetch_all(): - """Holt Flugdaten fuer alle Stuetzpunkte.""" + """Holt alle Flugzeuge weltweit von OpenSky Network.""" now = time.time() - if _cache["data"] and now - _cache["ts"] < 170: + if _cache["data"] and now - _cache["ts"] < 14: return _cache["data"] async with _lock: - if _cache["data"] and time.time() - _cache["ts"] < 25: + if _cache["data"] and time.time() - _cache["ts"] < 14: 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: - for ac in r.json().get("ac", []): - h = ac.get("hex") - if h and h not in seen: - seen[h] = ac - except Exception: - errors += 1 - if i + 3 < len(grid): - await asyncio.sleep(5.0) + 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} - _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() - logger.info(f"Flights: {len(seen)} Flugzeuge ({errors} Fehler)") + logger.info(f"Flights: {len(ac)} Flugzeuge (OpenSky)") return _cache["data"] async def _collector_loop(): - """Background-Loop: Flugdaten alle 30s vorladen.""" - await asyncio.sleep(5) + """Background-Loop: Flugdaten alle 15s vorladen.""" + await asyncio.sleep(3) while True: try: await _fetch_all() except Exception as e: logger.warning(f"Flight collector error: {e}") - await asyncio.sleep(180) + 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") + logger.info("Flight collector gestartet (OpenSky)") @router.get("/flights")