feat: domestic flights, road traffic, CCTV webcams, AI situation brief
Four new intelligence domains for the dashboard: 1. Domestic flights (OpenSky) — global airborne aircraft count by region with commercial/general breakdown. No API key needed. 2. Road traffic (TomTom) — real-time congestion % for 20 major world cities + traffic incidents in 5 strategic regions. Needs TOMTOM_API_KEY (free 2500 req/day at developer.tomtom.com). 3. CCTV webcams (Windy) — public traffic camera locations worldwide. Needs WINDY_API_KEY (free 100 req/day at api.windy.com). 4. AI situation brief (Ollama) — LLM-generated 3-paragraph intelligence brief synthesizing all dashboard data. Uses local Ollama (llama3.2). Falls back to structured metrics summary. New files: sources/traffic.py, sources/webcams.py, analysis/situation.py Modified: sources/aviation.py (+fetch_domestic_flights), dashboard/app.py, dashboard/index.html (drawer sections + HUD pills for all 4 domains). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
fc105872e2
commit
fcc702b093
@@ -1,11 +1,14 @@
|
||||
"""FAA airport delay data source for world-intel-mcp.
|
||||
"""Aviation data sources for world-intel-mcp.
|
||||
|
||||
Provides real-time US airport delay information from the FAA Airport
|
||||
Status Web Service (ASWS) API. No API key required.
|
||||
Status Web Service (ASWS) API, and global domestic air traffic counts
|
||||
from OpenSky Network. No API key required for either.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ..fetcher import Fetcher
|
||||
@@ -131,3 +134,119 @@ async def fetch_airport_delays(fetcher: Fetcher) -> dict:
|
||||
"source": "faa",
|
||||
"timestamp": now_iso,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domestic / commercial air traffic (OpenSky Network)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_OPENSKY_STATES_URL = "https://opensky-network.org/api/states/all"
|
||||
|
||||
_AIR_REGIONS = {
|
||||
"north_america": (15, -170, 72, -50),
|
||||
"europe": (35, -25, 72, 45),
|
||||
"east_asia": (15, 95, 55, 155),
|
||||
"middle_east": (12, 25, 42, 65),
|
||||
"south_asia": (5, 60, 40, 100),
|
||||
"africa": (-35, -20, 37, 55),
|
||||
"south_america": (-56, -82, 15, -34),
|
||||
"oceania": (-50, 110, 0, 180),
|
||||
}
|
||||
|
||||
_COMMERCIAL_PREFIXES = [
|
||||
"UAL", "AAL", "DAL", "SWA", "JBU", "ASA", "NKS", "FFT", "SKW",
|
||||
"BAW", "EZY", "RYR", "DLH", "AFR", "KLM", "SAS", "AUA", "TAP",
|
||||
"QFA", "ANZ", "JST", "VOZ", "CPA", "SIA", "THA", "ANA", "JAL",
|
||||
"CES", "CSN", "CCA", "HDA", "AIC", "UAE", "ETH", "SAA", "RAM",
|
||||
"TAM", "GLO", "AZU", "AVA", "LAN", "THY", "TRK", "SHT",
|
||||
]
|
||||
|
||||
|
||||
def _opensky_auth_headers() -> dict[str, str] | None:
|
||||
username = os.environ.get("OPENSKY_USERNAME")
|
||||
password = os.environ.get("OPENSKY_PASSWORD")
|
||||
if username and password:
|
||||
cred = base64.b64encode(f"{username}:{password}".encode()).decode()
|
||||
return {"Authorization": f"Basic {cred}"}
|
||||
return None
|
||||
|
||||
|
||||
def _classify_region(lat: float | None, lon: float | None) -> str:
|
||||
if lat is None or lon is None:
|
||||
return "unknown"
|
||||
for name, (lat_min, lon_min, lat_max, lon_max) in _AIR_REGIONS.items():
|
||||
if lat_min <= lat <= lat_max and lon_min <= lon <= lon_max:
|
||||
return name
|
||||
return "other"
|
||||
|
||||
|
||||
def _is_commercial(callsign: str | None) -> bool:
|
||||
if not callsign:
|
||||
return False
|
||||
cs = callsign.strip().upper()
|
||||
return any(cs.startswith(p) for p in _COMMERCIAL_PREFIXES)
|
||||
|
||||
|
||||
async def fetch_domestic_flights(fetcher: Fetcher) -> dict:
|
||||
"""Fetch global air traffic counts from OpenSky Network.
|
||||
|
||||
Queries all airborne aircraft once, then buckets by region and type.
|
||||
"""
|
||||
data = await fetcher.get_json(
|
||||
_OPENSKY_STATES_URL,
|
||||
source="opensky-domestic",
|
||||
cache_key="aviation:opensky:all",
|
||||
cache_ttl=120,
|
||||
headers=_opensky_auth_headers(),
|
||||
)
|
||||
|
||||
if data is None or not isinstance(data, dict):
|
||||
return {
|
||||
"total_aircraft": 0,
|
||||
"by_region": {},
|
||||
"busiest_origins": [],
|
||||
"error": "OpenSky API unavailable",
|
||||
"source": "opensky-domestic",
|
||||
"timestamp": _utc_now_iso(),
|
||||
}
|
||||
|
||||
states = data.get("states") or []
|
||||
|
||||
by_region: dict[str, dict] = {r: {"count": 0, "commercial": 0, "general": 0} for r in _AIR_REGIONS}
|
||||
by_region["other"] = {"count": 0, "commercial": 0, "general": 0}
|
||||
by_region["unknown"] = {"count": 0, "commercial": 0, "general": 0}
|
||||
country_counts: dict[str, int] = {}
|
||||
total = 0
|
||||
|
||||
for s in states:
|
||||
if not isinstance(s, list) or len(s) < 15:
|
||||
continue
|
||||
if s[8]: # on_ground
|
||||
continue
|
||||
|
||||
total += 1
|
||||
lat, lon = s[6], s[5]
|
||||
callsign = s[1]
|
||||
origin = s[2] or "Unknown"
|
||||
|
||||
region = _classify_region(lat, lon)
|
||||
by_region[region]["count"] += 1
|
||||
if _is_commercial(callsign):
|
||||
by_region[region]["commercial"] += 1
|
||||
else:
|
||||
by_region[region]["general"] += 1
|
||||
|
||||
country_counts[origin] = country_counts.get(origin, 0) + 1
|
||||
|
||||
# Remove empty regions
|
||||
by_region = {k: v for k, v in by_region.items() if v["count"] > 0}
|
||||
|
||||
busiest = sorted(country_counts.items(), key=lambda x: -x[1])[:15]
|
||||
|
||||
return {
|
||||
"total_aircraft": total,
|
||||
"by_region": by_region,
|
||||
"busiest_origins": [{"country": c, "count": n} for c, n in busiest],
|
||||
"source": "opensky-domestic",
|
||||
"timestamp": _utc_now_iso(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user