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 asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
@@ -9,82 +9,87 @@ from fastapi import APIRouter
|
|||||||
logger = logging.getLogger("globe.flights")
|
logger = logging.getLogger("globe.flights")
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# 64 Stuetzpunkte fuer globale Abdeckung (je 250nm Radius)
|
# OpenSky Network: Ein Call = alle Flugzeuge weltweit
|
||||||
_GRID = [
|
# Anonym: max 1 Call / 10 Sekunden, ~7000+ Flugzeuge
|
||||||
(48,2),(48,16),(55,10),(40,-4),(41,12),(38,24),(55,25),(60,25),(52,30),(45,37),
|
_OPENSKY_URL = "https://opensky-network.org/api/states/all"
|
||||||
(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),
|
|
||||||
]
|
|
||||||
|
|
||||||
_cache: dict = {"data": None, "ts": 0}
|
_cache: dict = {"data": None, "ts": 0}
|
||||||
_lock = asyncio.Lock()
|
_lock = asyncio.Lock()
|
||||||
_task = None
|
_task = None
|
||||||
|
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
async def _fetch_all():
|
async def _fetch_all():
|
||||||
"""Holt Flugdaten fuer alle Stuetzpunkte."""
|
"""Holt alle Flugzeuge weltweit von OpenSky Network."""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if _cache["data"] and now - _cache["ts"] < 170:
|
if _cache["data"] and now - _cache["ts"] < 14:
|
||||||
return _cache["data"]
|
return _cache["data"]
|
||||||
|
|
||||||
async with _lock:
|
async with _lock:
|
||||||
if _cache["data"] and time.time() - _cache["ts"] < 25:
|
if _cache["data"] and time.time() - _cache["ts"] < 14:
|
||||||
return _cache["data"]
|
return _cache["data"]
|
||||||
|
|
||||||
seen = {}
|
try:
|
||||||
errors = 0
|
async with httpx.AsyncClient(timeout=20) as client:
|
||||||
grid = list(_GRID)
|
resp = await client.get(_OPENSKY_URL)
|
||||||
random.shuffle(grid)
|
resp.raise_for_status()
|
||||||
async with httpx.AsyncClient(timeout=10) as client:
|
raw = resp.json()
|
||||||
for i in range(0, len(grid), 3):
|
except Exception as e:
|
||||||
batch = grid[i:i+3]
|
logger.warning(f"OpenSky Fehler: {e}")
|
||||||
tasks = [client.get(f"https://api.airplanes.live/v2/point/{lat:.2f}/{lon:.2f}/250")
|
return _cache["data"] or {"ac": [], "total": 0}
|
||||||
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)
|
|
||||||
|
|
||||||
_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()
|
_cache["ts"] = time.time()
|
||||||
logger.info(f"Flights: {len(seen)} Flugzeuge ({errors} Fehler)")
|
logger.info(f"Flights: {len(ac)} Flugzeuge (OpenSky)")
|
||||||
return _cache["data"]
|
return _cache["data"]
|
||||||
|
|
||||||
|
|
||||||
async def _collector_loop():
|
async def _collector_loop():
|
||||||
"""Background-Loop: Flugdaten alle 30s vorladen."""
|
"""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 error: {e}")
|
||||||
await asyncio.sleep(180)
|
await asyncio.sleep(15)
|
||||||
|
|
||||||
|
|
||||||
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")
|
logger.info("Flight collector gestartet (OpenSky)")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/flights")
|
@router.get("/flights")
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren