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:
@@ -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)
|
||||
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")
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren