diff --git a/scripts/save_fixtures.py b/scripts/save_fixtures.py index 26a1f5a..c966998 100644 --- a/scripts/save_fixtures.py +++ b/scripts/save_fixtures.py @@ -13,9 +13,13 @@ import sys from dataclasses import dataclass +from dotenv import load_dotenv + from jetset.config import AppConfig from jetset.fetcher import AirLabsAdapter +load_dotenv() + FIXTURES_DIR = "tests/fixtures" diff --git a/src/jetset/app.py b/src/jetset/app.py index cb08f7a..b067b60 100644 --- a/src/jetset/app.py +++ b/src/jetset/app.py @@ -18,7 +18,7 @@ class App: _FETCH_POLL_SECONDS = 1.0 # Display rotates a sliding window of this many flights; 4 metric pages each. WINDOW_SIZE = 5 - PAGES_PER_FLIGHT = 4 + PAGES_PER_FLIGHT = 5 class Frame(NamedTuple): flight: Flight @@ -93,16 +93,16 @@ def _current_frame(self) -> Frame | None: return None window = min(self.WINDOW_SIZE, n) - # Slide the window across all captured flights once per refresh interval - # so fresh flights trickle into the rotation between (infrequent) fetches. - # The slide cadence scales with the catch size (refresh / N). - slide_interval = self.config.refresh / n - since = self.last_fetch if self.last_fetch is not None else time.time() - elapsed = time.time() - since - window_start = int(elapsed / slide_interval) % n if slide_interval else 0 - - # Within the window, rotate one flight at a time through its metric pages. - flight_in_window = (self.frame // self.PAGES_PER_FLIGHT) % window + # Show a sliding window of flights, one at a time. Each flight gets + # cycles_per_flight * PAGES_PER_FLIGHT frames (e.g. 1 cycle = all 4 + # metric pages once) before advancing to the next. After every flight + # in the current window has been shown, slide the window by 1 so fresh + # flights trickle in. + frames_per_flight = self.PAGES_PER_FLIGHT * self.config.cycles_per_flight + frames_per_window_cycle = frames_per_flight * window + cycle = self.frame // frames_per_window_cycle + window_start = cycle % max(1, n - window + 1) if n else 0 + flight_in_window = (self.frame % frames_per_window_cycle) // frames_per_flight metric_page = self.frame % self.PAGES_PER_FLIGHT return self.Frame(flights[(window_start + flight_in_window) % n], metric_page) diff --git a/src/jetset/config.py b/src/jetset/config.py index 2509949..84c038d 100644 --- a/src/jetset/config.py +++ b/src/jetset/config.py @@ -5,11 +5,12 @@ @dataclass(frozen=True) class AppConfig: - # IAH - home_lat: float = 29.9931 - home_lon: float = -95.3416 - range: int = 200 - pause: int = 2 + # HEB + home_lat: float = 30.0689232902742 + home_lon: float = -95.25178382821562 + range: int = 100 # in km + pause: int = 5 # how long in seconds to wait between cycling through the pages of a flight + cycles_per_flight: int = 2 # how many times to show all details per flight refresh: int = 2700 # 45 min — one AirLabs bbox call per refresh ≈ 960/month api_source: str = "airlabs" # Where airline logos are cached. A global path (not a user home) so the diff --git a/src/jetset/display.py b/src/jetset/display.py index d832465..2d2fe1f 100644 --- a/src/jetset/display.py +++ b/src/jetset/display.py @@ -27,7 +27,7 @@ def load_logo(airline_code: str, logo_dir: Path) -> Image.Image | None: path = logo_dir / f"{airline_code}.png" try: return Image.open(path) - except (FileNotFoundError, PermissionError): + except FileNotFoundError, PermissionError: return None @@ -69,6 +69,7 @@ def metrics_label(flight: Flight, page: int = 0) -> str: 1 = speed (e.g. "450kn") 2 = vertical rate (e.g. "▲1500ft/min", "▼1200ft/min", or "▬0ft/min") 3 = track (e.g. "270°W") + 4 = distance from home (e.g. "32km away") Returns an empty string if the relevant field is missing. """ @@ -87,6 +88,8 @@ def metrics_label(flight: Flight, page: int = 0) -> str: marker = _LEVEL data += f"{marker}{abs(rate)}ft/min" elif page == 3 and flight.track is not None: - data += f"{int(flight.track)}\u00b0{_cardinal(flight.track)}" + data += f"{int(flight.track)}°{_cardinal(flight.track)}" + elif page == 4 and flight.distance_km is not None: + data += f"{round(flight.distance_km)}km away" return data diff --git a/src/jetset/fetcher.py b/src/jetset/fetcher.py index 57e2699..bcc180b 100644 --- a/src/jetset/fetcher.py +++ b/src/jetset/fetcher.py @@ -33,6 +33,20 @@ def _ms_to_ft_per_min(ms: float | None) -> int | None: return round(ms * 196.850394) if ms is not None else None +def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + """Great-circle distance between two points in km.""" + r = 6371 + dlat = math.radians(lat2 - lat1) + dlon = math.radians(lon2 - lon1) + a = ( + math.sin(dlat / 2) ** 2 + + math.cos(math.radians(lat1)) + * math.cos(math.radians(lat2)) + * math.sin(dlon / 2) ** 2 + ) + return r * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + class AirLabsAdapter(FlightAPI): """Single data source for the display. @@ -43,9 +57,9 @@ class AirLabsAdapter(FlightAPI): interval — one bbox call per refresh. """ - def __init__(self) -> None: + def __init__(self, api_key: str | None = None) -> None: self._api = RequestsAPI("https://airlabs.co/api/v9") - self._api_key = os.environ.get("AIRLABS_API_KEY") + self._api_key = api_key or os.environ.get("AIRLABS_API_KEY") @staticmethod def _bbox(lat: float, lon: float, range_nm: float) -> str: @@ -55,18 +69,30 @@ def _bbox(lat: float, lon: float, range_nm: float) -> str: return f"{lat - half_lat},{lon - half_lon},{lat + half_lat},{lon + half_lon}" @staticmethod - def to_flight(data: dict) -> Flight: + def to_flight( + data: dict, + home_lat: float | None = None, + home_lon: float | None = None, + ) -> Flight: """Map an AirLabs flight (metric units) to a Flight (feet/knots/ft-min).""" dep, arr = data.get("dep_iata"), data.get("arr_iata") route = FlightRoute(Airport(dep), Airport(arr)) if dep and arr else None + f_lat, f_lng = data.get("lat"), data.get("lng") + distance_km = ( + _haversine_km(home_lat, home_lon, f_lat, f_lng) + if home_lat is not None and home_lon is not None + and f_lat is not None and f_lng is not None + else None + ) return Flight( callsign=(data.get("flight_icao") or "").strip(), aircraft=data.get("aircraft_icao"), route=route, altitude=_meters_to_feet(data.get("alt")), speed=_kmh_to_knots(data.get("speed")), - track=data.get("dir"), + track=float(d) if (d := data.get("dir")) is not None else None, vertical_rate=_ms_to_ft_per_min(data.get("v_speed")), + distance_km=distance_km, ) def nearby_flights( @@ -88,10 +114,21 @@ def nearby_flights( detail = body.get("error") if isinstance(body, dict) else body logger.warning("AirLabs returned no usable data: %s", detail) return [] - raw_flights = [f for f in (body.get("response") or []) if f.get("flight_icao")] + raw_flights = [ + f + for f in (body.get("response") or []) + if f.get("flight_icao") and f.get("status") == "en-route" + ] + # Sort by proximity to home so the display shows the closest + # flights first. Flights without coordinates go to the end. + raw_flights.sort( + key=lambda f: _haversine_km(lat, lon, f.get("lat", 0), f.get("lng", 0)) + if f.get("lat") is not None and f.get("lng") is not None + else float("inf") + ) if raw: return raw_flights - return [self.to_flight(f) for f in raw_flights] + return [self.to_flight(f, lat, lon) for f in raw_flights] except (requests.exceptions.RequestException, ValueError) as e: logger.warning("Error fetching flights from AirLabs: %s", e) return [] diff --git a/src/jetset/models.py b/src/jetset/models.py index 8b10644..1acce7c 100644 --- a/src/jetset/models.py +++ b/src/jetset/models.py @@ -13,6 +13,7 @@ class Flight: speed: int | None = None track: float | None = None vertical_rate: int | None = None + distance_km: float | None = None def __post_init__(self) -> None: logger.debug( diff --git a/tests/fixtures/airlabs_response.json b/tests/fixtures/airlabs_response.json index 2a8c12d..6df39db 100644 --- a/tests/fixtures/airlabs_response.json +++ b/tests/fixtures/airlabs_response.json @@ -1,55 +1,2134 @@ [ { - "hex": "0D0ACA", - "reg_number": "XA-VAX", - "lat": 29.566162, - "lng": -95.341946, - "alt": 5559, - "dir": 228.9, - "speed": 621, - "v_speed": 0, - "flight_number": "613", - "flight_icao": "VIV613", - "flight_iata": "VB613", - "dep_icao": "KIAH", - "dep_iata": "IAH", - "arr_icao": "MMMY", - "arr_iata": "MTY", - "airline_icao": "VIV", - "airline_iata": "VB", + "hex": "A6981A", + "reg_number": "N524DE", + "flag": "US", + "lat": 30.393883, + "lng": -96.254449, + "alt": 10055, + "dir": 102.3, + "speed": 936, + "v_speed": 0, + "flight_number": "867", + "flight_icao": "DAL867", + "flight_iata": "DL867", + "dep_icao": "KLAX", + "dep_iata": "LAX", + "arr_icao": "KMCO", + "arr_iata": "MCO", + "airline_icao": "DAL", + "airline_iata": "DL", + "aircraft_icao": "A21N", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A2B12D", + "reg_number": "N27260", + "flag": "US", + "lat": 28.877461, + "lng": -94.449137, + "alt": 6878, + "dir": 137.9, + "speed": 845, + "v_speed": 0, + "flight_number": "1632", + "flight_icao": "UAL1632", + "flight_iata": "UA1632", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "MGGT", + "arr_iata": "GUA", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B38M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "C08852", + "reg_number": "C-GZQF", + "flag": "CA", + "lat": 30.837385, + "lng": -97.353107, + "alt": 3987, + "dir": 197.7, + "speed": 578, + "v_speed": 0, + "flight_number": "627", + "flight_icao": "POE627", + "flight_iata": "P3627", + "dep_icao": "CYYZ", + "dep_iata": "YYZ", + "arr_icao": "KAUS", + "arr_iata": "AUS", + "airline_icao": "POE", + "airline_iata": "PD", + "aircraft_icao": "E295", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A12E80", + "reg_number": "N17529", + "flag": "US", + "lat": 31.02672, + "lng": -96.042026, + "alt": 10985, + "dir": 306.9, + "speed": 820, + "v_speed": 0, + "flight_number": "1948", + "flight_icao": "UAL1948", + "flight_iata": "UA1948", + "dep_icao": "MMUN", + "dep_iata": "CUN", + "arr_icao": "KSFO", + "arr_iata": "SFO", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B39M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A5F208", + "reg_number": "N482WN", + "flag": "US", + "lat": 29.969139, + "lng": -96.595192, + "alt": 6176, + "dir": 289.7, + "speed": 752, + "v_speed": 0, + "flight_number": "399", + "flight_icao": "SWA399", + "flight_iata": "WN399", + "dep_icao": "KAUS", + "dep_iata": "BWI", + "arr_icao": "KSJC", + "arr_iata": "DAL", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AAA2D3", + "reg_number": "N7848A", + "flag": "US", + "lat": 31.11642, + "lng": -95.963777, + "alt": 10665, + "dir": 337.5, + "speed": 840, + "v_speed": 0, + "flight_number": "4400", + "flight_icao": "SWA4400", + "flight_iata": "WN4400", + "dep_icao": "KHOU", + "dep_iata": "HOU", + "arr_icao": "KOKC", + "arr_iata": "OKC", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC049E", + "reg_number": "N87353", + "flag": "US", + "lat": 30.466286, + "lng": -95.846271, + "alt": 5994, + "dir": 138.2, + "speed": 689, + "v_speed": 0, + "flight_number": "4061", + "flight_icao": "ASH4061", + "flight_iata": "UA4061", + "dep_icao": "KMAF", + "dep_iata": "MAF", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "ABBCA1", + "reg_number": "N8552Z", + "flag": "US", + "lat": 30.645809, + "lng": -95.711069, + "alt": 7838, + "dir": 336.4, + "speed": 747, + "v_speed": 0, + "flight_number": "3566", + "flight_icao": "SWA3566", + "flight_iata": "WN3566", + "dep_icao": "KHOU", + "dep_iata": "HOU", + "arr_icao": "KSEA", + "arr_iata": "SEA", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A28B5B", + "reg_number": "N263NN", + "flag": "US", + "lat": 30.261241, + "lng": -95.645741, + "alt": 4208, + "dir": 139, + "speed": 573, + "v_speed": 0, + "flight_number": "3583", + "flight_icao": "ENY3583", + "flight_iata": "AA3583", + "dep_icao": "KDFW", + "dep_iata": "DFW", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "ENY", + "airline_iata": "MQ", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AB1DB2", + "reg_number": "N815UA", + "flag": "US", + "lat": 30.093137, + "lng": -95.335907, + "alt": 1960, + "dir": 88.8, + "speed": 513, + "v_speed": 0, + "flight_number": "1598", + "flight_icao": "UAL1598", + "flight_iata": "UA1598", + "dep_icao": "KDRO", + "dep_iata": "DRO", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "A319", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC04BE", + "reg_number": "N87360", + "flag": "US", + "lat": 30.007205, + "lng": -95.320008, + "alt": 79, + "dir": 272.6, + "speed": 261, + "v_speed": 0, + "flight_number": "4027", + "flight_icao": "ASH4027", + "flight_iata": "UA4027", + "dep_icao": "KMTJ", + "dep_iata": "MTJ", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A7668A", + "reg_number": "N576UW", + "flag": "US", + "lat": 31.194268, + "lng": -95.221775, + "alt": 10977, + "dir": 296.6, + "speed": 823, + "v_speed": 0, + "flight_number": "2081", + "flight_icao": "AAL2081", + "flight_iata": "AA2081", + "dep_icao": "KMCO", + "dep_iata": "MCO", + "arr_icao": "KDFW", + "arr_iata": "DFW", + "airline_icao": "AAL", + "airline_iata": "AA", + "aircraft_icao": "A321", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC537F", + "reg_number": "N89349", + "flag": "US", + "lat": 30.092747, + "lng": -95.203811, + "alt": 1556, + "dir": 87.8, + "speed": 440, + "v_speed": 0, + "flight_number": "4030", + "flight_icao": "ASH4030", + "flight_iata": "UA4030", + "dep_icao": "KCOS", + "dep_iata": "COS", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC2BB9", + "reg_number": "N88328", + "flag": "US", + "lat": 30.007382, + "lng": -95.123078, + "alt": 680, + "dir": 267.8, + "speed": 338, + "v_speed": 0, + "flight_number": "6151", + "flight_icao": "ASH6151", + "flight_iata": "UA6151", + "dep_icao": "KABQ", + "dep_iata": "ABQ", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E75L", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC0417", + "reg_number": "N87318", + "flag": "US", + "lat": 30.013227, + "lng": -95.044234, + "alt": 650, + "dir": 245.7, + "speed": 397, + "v_speed": 0, + "flight_number": "4002", + "flight_icao": "ASH4002", + "flight_iata": "UA4002", + "dep_icao": "KELP", + "dep_iata": "ELP", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A62BDC", + "reg_number": "N497UA", + "flag": "US", + "lat": 30.864682, + "lng": -94.89751, + "alt": 6504, + "dir": 30, + "speed": 739, + "v_speed": 0, + "flight_number": "1934", + "flight_icao": "UAL1934", + "flight_iata": "UA1934", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KCLE", + "arr_iata": "CLE", + "airline_icao": "UAL", + "airline_iata": "UA", "aircraft_icao": "A320", - "status": "en-route" + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A1C03A", + "reg_number": "N21144", + "flag": "US", + "lat": 30.88243, + "lng": -94.627652, + "alt": 6466, + "dir": 43.5, + "speed": 698, + "v_speed": 0, + "flight_number": "4814", + "flight_icao": "UCA4814", + "flight_iata": "UA4814", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KJAN", + "arr_iata": "JAN", + "airline_icao": "UCA", + "airline_iata": "C5", + "aircraft_icao": "E145", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A5031A", + "reg_number": "N422AN", + "flag": "US", + "lat": 30.208763, + "lng": -94.471625, + "alt": 8859, + "dir": 105, + "speed": 893, + "v_speed": 0, + "flight_number": "3016", + "flight_icao": "AAL3016", + "flight_iata": "AA3016", + "dep_icao": "KLAX", + "dep_iata": "LAX", + "arr_icao": "KMIA", + "arr_iata": "MIA", + "airline_icao": "AAL", + "airline_iata": "AA", + "aircraft_icao": "A21N", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC03EF", + "reg_number": "N87303", + "flag": "US", + "lat": 31.589147, + "lng": -94.420916, + "alt": 9362, + "dir": 31.8, + "speed": 871, + "v_speed": 0, + "flight_number": "6164", + "flight_icao": "ASH6164", + "flight_iata": "UA6164", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KMKE", + "arr_iata": "MKE", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A3A9AF", + "reg_number": "N335PH", + "flag": "US", + "lat": 31.676627, + "lng": -94.407047, + "alt": 10748, + "dir": 299.4, + "speed": 804, + "v_speed": 0, + "flight_number": "2438", + "flight_icao": "AAL2438", + "flight_iata": "AA2438", + "dep_icao": "KRSW", + "dep_iata": "RSW", + "arr_icao": "KDFW", + "arr_iata": "DFW", + "airline_icao": "AAL", + "airline_iata": "AA", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AA568F", + "reg_number": "N76503", + "flag": "US", + "lat": 31.096398, + "lng": -94.350056, + "alt": 10992, + "dir": 283, + "speed": 776, + "v_speed": 0, + "flight_number": "2325", + "flight_icao": "UAL2325", + "flight_iata": "UA2325", + "dep_icao": "KMSY", + "dep_iata": "MSY", + "arr_icao": "KDEN", + "arr_iata": "DEN", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A0F8D3", + "reg_number": "N16178", + "flag": "US", + "lat": 31.506027, + "lng": -94.152304, + "alt": 9941, + "dir": 34.8, + "speed": 746, + "v_speed": 0, + "flight_number": "4239", + "flight_icao": "UCA4239", + "flight_iata": "UA4239", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KBHM", + "arr_iata": "BHM", + "airline_icao": "UCA", + "airline_iata": "C5", + "aircraft_icao": "E145", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A2B3A2", + "reg_number": "N273WN", + "flag": "US", + "lat": 31.640667, + "lng": -93.376535, + "alt": 11907, + "dir": 167, + "speed": 831, + "v_speed": 0, + "flight_number": "3120", + "flight_icao": "SWA3120", + "flight_iata": "WN3120", + "dep_icao": "KOKC", + "dep_iata": "OKC", + "arr_icao": "KMCO", + "arr_iata": "MCO", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A448D5", + "reg_number": "N37506", + "flag": "US", + "lat": 31.494988, + "lng": -93.29471, + "alt": 8554, + "dir": 229, + "speed": 865, + "v_speed": 0, + "flight_number": "2038", + "flight_icao": "UAL2038", + "flight_iata": "UA2038", + "dep_icao": "KPHL", + "dep_iata": "PHL", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B39M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A05776", + "reg_number": "N121AN", + "flag": "US", + "lat": 30.462478, + "lng": -93.283533, + "alt": 10383, + "dir": 295, + "speed": 819, + "v_speed": 0, + "flight_number": "1846", + "flight_icao": "AAL1846", + "flight_iata": "AA1846", + "dep_icao": "KTPA", + "dep_iata": "TPA", + "arr_icao": "KDFW", + "arr_iata": "DFW", + "airline_icao": "AAL", + "airline_iata": "AA", + "aircraft_icao": "A321", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AB79DE", + "reg_number": "N839AW", + "flag": "US", + "lat": 29.660882, + "lng": -95.953597, + "alt": 10459, + "dir": 91.5, + "speed": 916, + "v_speed": 0, + "flight_number": "2486", + "flight_icao": "AAL2486", + "flight_iata": "AA2486", + "dep_icao": "KSAT", + "dep_iata": "SAT", + "arr_icao": "KORD", + "arr_iata": "ORD", + "airline_icao": "AAL", + "airline_iata": "AA", + "aircraft_icao": "A319", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "ABC432", + "reg_number": "N8573Z", + "flag": "US", + "lat": 29.804822, + "lng": -95.950504, + "alt": 5460, + "dir": 301.6, + "speed": 763, + "v_speed": 0, + "flight_number": "3304", + "flight_icao": "SWA3304", + "flight_iata": "WN3304", + "dep_icao": "KHOU", + "dep_iata": "HOU", + "arr_icao": "KPHX", + "arr_iata": "PHX", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AA1F5A", + "reg_number": "N751SK", + "flag": "US", + "lat": 29.641471, + "lng": -95.65284, + "alt": 1953, + "dir": 92, + "speed": 508, + "v_speed": 0, + "flight_number": "5002", + "flight_icao": "SKW5002", + "flight_iata": "AA5002", + "dep_icao": "KDFW", + "dep_iata": "DFW", + "arr_icao": "KHOU", + "arr_iata": "HOU", + "airline_icao": "SKW", + "airline_iata": "OO", + "aircraft_icao": "CRJ7", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A1BF3A", + "reg_number": "N211WN", + "flag": "US", + "lat": 29.813468, + "lng": -95.543122, + "alt": 11335, + "dir": 51.2, + "speed": 942, + "v_speed": 0, + "flight_number": "481", + "flight_icao": "SWA481", + "flight_iata": "WN481", + "dep_icao": "KSAT", + "dep_iata": "SAT", + "arr_icao": "KBNA", + "arr_iata": "BNA", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A42B32", + "reg_number": "N368NB", + "flag": "US", + "lat": 29.720052, + "lng": -95.5247, + "alt": 11983, + "dir": 26.6, + "speed": 873, + "v_speed": 0, + "flight_number": "645", + "flight_icao": "DAL645", + "flight_iata": "DL645", + "dep_icao": "MMMX", + "dep_iata": "MEX", + "arr_icao": "KDTW", + "arr_iata": "DTW", + "airline_icao": "DAL", + "airline_iata": "DL", + "aircraft_icao": "A319", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC3731", + "reg_number": "N8865L", + "flag": "US", + "lat": 29.682174, + "lng": -95.467582, + "alt": 11602, + "dir": 280, + "speed": 795, + "v_speed": 0, + "flight_number": "4045", + "flight_icao": "SWA4045", + "flight_iata": "WN4045", + "dep_icao": "KTPA", + "dep_iata": "TPA", + "arr_icao": "KSAT", + "arr_iata": "SAT", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B38M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A5F3CF", + "reg_number": "N483AS", + "flag": "US", + "lat": 29.954929, + "lng": -95.330822, + "alt": 10688, + "dir": 99.7, + "speed": 957, + "v_speed": 0, + "flight_number": "396", + "flight_icao": "ASA396", + "flight_iata": "AS396", + "dep_icao": "KSAN", + "dep_iata": "SAN", + "arr_icao": "KMCO", + "arr_iata": "MCO", + "airline_icao": "ASA", + "airline_iata": "AS", + "aircraft_icao": "B739", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "ABFD23", + "reg_number": "N8715Q", + "flag": "US", + "lat": 29.664467, + "lng": -95.257366, + "alt": 86, + "dir": 224.9, + "speed": 275, + "v_speed": 0, + "flight_number": "3067", + "flight_icao": "SWA3067", + "flight_iata": "WN3067", + "dep_icao": "KPHX", + "dep_iata": "PHX", + "arr_icao": "KHOU", + "arr_iata": "HOU", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B38M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "ABE0BB", + "reg_number": "N8645A", + "flag": "US", + "lat": 29.769551, + "lng": -95.228201, + "alt": 2707, + "dir": 189.5, + "speed": 548, + "v_speed": 0, + "flight_number": "4254", + "flight_icao": "SWA4254", + "flight_iata": "WN4254", + "dep_icao": "KBWI", + "dep_iata": "BWI", + "arr_icao": "KHOU", + "arr_iata": "HOU", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A441AC", + "reg_number": "N37325", + "flag": "US", + "lat": 29.993501, + "lng": -95.216021, + "alt": 619, + "dir": 269, + "speed": 325, + "v_speed": 0, + "flight_number": "1627", + "flight_icao": "UAL1627", + "flight_iata": "UA1627", + "dep_icao": "KIAH", + "dep_iata": "LGA", + "arr_icao": "KDCA", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B38M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AD7DA2", + "reg_number": "N969AN", + "flag": "US", + "lat": 29.334793, + "lng": -95.176123, + "alt": 11358, + "dir": 142.3, + "speed": 889, + "v_speed": 0, + "flight_number": "1053", + "flight_icao": "AAL1053", + "flight_iata": "AA1053", + "dep_icao": "KDFW", + "dep_iata": "DFW", + "arr_icao": "MROC", + "arr_iata": "SJO", + "airline_icao": "AAL", + "airline_iata": "AA", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AB3EE2", + "reg_number": "N82338", + "flag": "US", + "lat": 29.993468, + "lng": -95.104511, + "alt": 1198, + "dir": 269.3, + "speed": 353, + "v_speed": 0, + "flight_number": "6281", + "flight_icao": "ASH6281", + "flight_iata": "UA6281", + "dep_icao": "KSAT", + "dep_iata": "SAT", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AAF002", + "reg_number": "N80343", + "flag": "US", + "lat": 29.993273, + "lng": -95.007927, + "alt": 1617, + "dir": 269.6, + "speed": 351, + "v_speed": 0, + "flight_number": "4044", + "flight_icao": "ASH4044", + "flight_iata": "UA4044", + "dep_icao": "KIAH", + "dep_iata": "TUS", + "arr_icao": "KLIT", + "arr_iata": "IAH", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A03398", + "reg_number": "N11192", + "flag": "US", + "lat": 29.891284, + "lng": -94.962328, + "alt": 1953, + "dir": 89.2, + "speed": 457, + "v_speed": 0, + "flight_number": "4980", + "flight_icao": "UCA4980", + "flight_iata": "UA4980", + "dep_icao": "KHRL", + "dep_iata": "HRL", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UCA", + "airline_iata": "C5", + "aircraft_icao": "E145", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A46D95", + "reg_number": "N38479", + "flag": "US", + "lat": 29.89674, + "lng": -94.708061, + "alt": 2577, + "dir": 327, + "speed": 455, + "v_speed": 0, + "flight_number": "1051", + "flight_icao": "UAL1051", + "flight_iata": "UA1051", + "dep_icao": "MMUN", + "dep_iata": "CUN", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B739", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A3A0CF", + "reg_number": "N33289", + "flag": "US", + "lat": 29.618739, + "lng": -94.450217, + "alt": 3736, + "dir": 303, + "speed": 555, + "v_speed": 0, + "flight_number": "1405", + "flight_icao": "UAL1405", + "flight_iata": "UA1405", + "dep_icao": "MZBZ", + "dep_iata": "BZE", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "ABA8FF", + "reg_number": "N850SY", + "flag": "US", + "lat": 29.131543, + "lng": -94.303027, + "alt": 11297, + "dir": 134, + "speed": 896, + "v_speed": 0, + "flight_number": "517", + "flight_icao": "SCX517", + "flight_iata": "SY517", + "dep_icao": "KDFW", + "dep_iata": "DFW", + "arr_icao": "MMUN", + "arr_iata": "CUN", + "airline_icao": "SCX", + "airline_iata": "SY", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AAB1D2", + "reg_number": "N7889A", + "flag": "US", + "lat": 29.792641, + "lng": -94.254321, + "alt": 10383, + "dir": 271, + "speed": 774, + "v_speed": 0, + "flight_number": "311", + "flight_icao": "SWA311", + "flight_iata": "WN311", + "dep_icao": "KMSY", + "dep_iata": "MSY", + "arr_icao": "KAUS", + "arr_iata": "AUS", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AA7EC2", + "reg_number": "N77558", + "flag": "US", + "lat": 28.595909, + "lng": -93.577985, + "alt": 10688, + "dir": 161, + "speed": 887, + "v_speed": 0, + "flight_number": "1987", + "flight_icao": "UAL1987", + "flight_iata": "UA1987", + "dep_icao": "KDEN", + "dep_iata": "DEN", + "arr_icao": "MMUN", + "arr_iata": "CUN", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B39M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A5CBF3", + "reg_number": "N47275", + "flag": "US", + "lat": 28.261313, + "lng": -93.450369, + "alt": 10383, + "dir": 340.5, + "speed": 828, + "v_speed": 0, + "flight_number": "1589", + "flight_icao": "UAL1589", + "flight_iata": "UA1589", + "dep_icao": "MROC", + "dep_iata": "SJO", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B38M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A05A49", + "reg_number": "N12136", + "flag": "US", + "lat": 30.238539, + "lng": -97.172555, + "alt": 8249, + "dir": 280.6, + "speed": 685, + "v_speed": 0, + "flight_number": "4830", + "flight_icao": "UCA4830", + "flight_iata": "UA4830", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KLBB", + "arr_iata": "LBB", + "airline_icao": "UCA", + "airline_iata": "C5", + "aircraft_icao": "E145", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AAA247", + "reg_number": "N7844A", + "flag": "US", + "lat": 31.634284, + "lng": -97.050769, + "alt": 10078, + "dir": 145, + "speed": 893, + "v_speed": 0, + "flight_number": "1544", + "flight_icao": "SWA1544", + "flight_iata": "WN1544", + "dep_icao": "KDEN", + "dep_iata": "DEN", + "arr_icao": "KHOU", + "arr_iata": "HOU", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A40D7A", + "reg_number": "N360NB", + "flag": "US", + "lat": 31.53397, + "lng": -96.979825, + "alt": 11632, + "dir": 212, + "speed": 800, + "v_speed": 0, + "flight_number": "2458", + "flight_icao": "DAL2458", + "flight_iata": "DL2458", + "dep_icao": "KDTW", + "dep_iata": "DTW", + "arr_icao": "KSAT", + "arr_iata": "SAT", + "airline_icao": "DAL", + "airline_iata": "DL", + "aircraft_icao": "A319", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC97E4", + "reg_number": "N910NN", + "flag": "US", + "lat": 31.384487, + "lng": -96.463682, + "alt": 9110, + "dir": 163, + "speed": 859, + "v_speed": 0, + "flight_number": "1507", + "flight_icao": "AAL1507", + "flight_iata": "AA1507", + "dep_icao": "KDFW", + "dep_iata": "DFW", + "arr_icao": "MMUN", + "arr_iata": "CUN", + "airline_icao": "AAL", + "airline_iata": "AA", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AD2862", + "reg_number": "N947NN", + "flag": "US", + "lat": 31.041368, + "lng": -96.337423, + "alt": 10421, + "dir": 163, + "speed": 881, + "v_speed": 0, + "flight_number": "2704", + "flight_icao": "AAL2704", + "flight_iata": "AA2704", + "dep_icao": "KDFW", + "dep_iata": "DFW", + "arr_icao": "KSRQ", + "arr_iata": "SRQ", + "airline_icao": "AAL", + "airline_iata": "AA", + "aircraft_icao": "B738", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC047D", + "reg_number": "N87345", + "flag": "US", + "lat": 30.104499, + "lng": -96.282188, + "alt": 5216, + "dir": 279, + "speed": 684, + "v_speed": 0, + "flight_number": "4006", + "flight_icao": "ASH4006", + "flight_iata": "UA4006", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KSLC", + "arr_iata": "SLC", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC0F4D", + "reg_number": "N8762Q", + "flag": "US", + "lat": 29.858892, + "lng": -97.279055, + "alt": 7327, + "dir": 276, + "speed": 726, + "v_speed": 0, + "flight_number": "2074", + "flight_icao": "SWA2074", + "flight_iata": "WN2074", + "dep_icao": "KHOU", + "dep_iata": "HOU", + "arr_icao": "KSAT", + "arr_iata": "SAT", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B38M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC2BD9", + "reg_number": "N88335", + "flag": "US", + "lat": 29.223878, + "lng": -96.552869, + "alt": 7335, + "dir": 45, + "speed": 838, + "v_speed": 0, + "flight_number": "4046", + "flight_icao": "ASH4046", + "flight_iata": "UA4046", + "dep_icao": "KMFE", + "dep_iata": "MFE", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC2BFD", + "reg_number": "N88346", + "flag": "US", + "lat": 29.371531, + "lng": -96.360593, + "alt": 7182, + "dir": 40, + "speed": 808, + "v_speed": 0, + "flight_number": "6153", + "flight_icao": "ASH6153", + "flight_iata": "UA6153", + "dep_icao": "MMAS", + "dep_iata": "AGU", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E75L", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A13651", + "reg_number": "N17753", + "flag": "US", + "lat": 29.515439, + "lng": -96.232426, + "alt": 6131, + "dir": 44.3, + "speed": 758, + "v_speed": 0, + "flight_number": "1725", + "flight_icao": "UAL1725", + "flight_iata": "UA1725", + "dep_icao": "MMGL", + "dep_iata": "GDL", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A0330A", + "reg_number": "N11150", + "flag": "US", + "lat": 28.982051, + "lng": -96.04691, + "alt": 4597, + "dir": 224.7, + "speed": 732, + "v_speed": 0, + "flight_number": "4825", + "flight_icao": "UCA4825", + "flight_iata": "UA4825", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KCRP", + "arr_iata": "CRP", + "airline_icao": "UCA", + "airline_iata": "C5", + "aircraft_icao": "E145", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "C03BFB", + "reg_number": "C-FWSO", + "flag": "CA", + "lat": 30.982704, + "lng": -95.891807, + "alt": 8211, + "dir": 336.1, + "speed": 810, + "v_speed": 0, + "flight_number": "1543", + "flight_icao": "WJA1543", + "flight_iata": "WS1543", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "CYYC", + "arr_iata": "YYC", + "airline_icao": "WJA", + "airline_iata": "WS", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "ABDCB5", + "reg_number": "N86322", + "flag": "US", + "lat": 30.789994, + "lng": -95.788142, + "alt": 5780, + "dir": 336.2, + "speed": 701, + "v_speed": 0, + "flight_number": "6270", + "flight_icao": "ASH6270", + "flight_iata": "YV6270", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KTUL", + "arr_iata": "TUL", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E75L", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A54169", + "reg_number": "N438FX", + "flag": "US", + "lat": 30.371779, + "lng": -95.564215, + "alt": 7731, + "dir": 336.1, + "speed": 729, + "v_speed": 0, + "flight_number": "438", + "flight_icao": "LXJ438", + "dep_icao": "KHOU", + "dep_iata": "IAH", + "arr_icao": "KCOS", + "arr_iata": "HOU", + "airline_icao": "LXJ", + "aircraft_icao": "E545", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AA2BC2", + "reg_number": "N75433", + "flag": "US", + "lat": 30.398467, + "lng": -95.555905, + "alt": 3652, + "dir": 327.9, + "speed": 612, + "v_speed": 0, + "flight_number": "2608", + "flight_icao": "UAL2608", + "flight_iata": "UA2608", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KEWR", + "arr_iata": "EWR", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B739", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AAC954", + "reg_number": "N79402", + "flag": "US", + "lat": 30.556801, + "lng": -95.481969, + "alt": 5140, + "dir": 351.6, + "speed": 679, + "v_speed": 0, + "flight_number": "1388", + "flight_icao": "UAL1388", + "flight_iata": "UA1388", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KDFW", + "arr_iata": "DFW", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B739", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC28E5", + "reg_number": "N8829Q", + "flag": "US", + "lat": 30.068871, + "lng": -95.212386, + "alt": 4391, + "dir": 28.1, + "speed": 667, + "v_speed": 0, + "flight_number": "437", + "flight_icao": "SWA437", + "flight_iata": "WN437", + "dep_icao": "KHOU", + "dep_iata": "HOU", + "arr_icao": "KBHM", + "arr_iata": "BHM", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B38M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A83E49", + "reg_number": "N630FR", + "flag": "US", + "lat": 30.350321, + "lng": -95.208357, + "alt": 3751, + "dir": 25.2, + "speed": 590, + "v_speed": 0, + "flight_number": "1958", + "flight_icao": "FFT1958", + "flight_iata": "F91958", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KDTW", + "arr_iata": "DTW", + "airline_icao": "FFT", + "airline_iata": "F9", + "aircraft_icao": "A21N", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A3282F", + "reg_number": "N302QS", + "flag": "US", + "lat": 31.423901, + "lng": -94.07181, + "alt": 13835, + "dir": 66, + "speed": 823, + "v_speed": 0, + "flight_number": "302", + "flight_icao": "EJA302", + "flight_iata": "1L302", + "dep_icao": "KBDR", + "dep_iata": "AUS", + "arr_icao": "KJPX", + "arr_iata": "BOS", + "airline_icao": "EJA", + "airline_iata": "1L", + "aircraft_icao": "E55P", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AB3D6F", + "reg_number": "N823QS", + "flag": "US", + "lat": 30.682224, + "lng": -93.910345, + "alt": 11396, + "dir": 219.9, + "speed": 845, + "v_speed": 0, + "flight_number": "823", + "flight_icao": "EJA823", + "flight_iata": "1L823", + "dep_icao": "KIAD", + "dep_iata": "TEB", + "arr_icao": "KBOS", + "arr_iata": "SGR", + "airline_icao": "EJA", + "airline_iata": "1L", + "aircraft_icao": "C700", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A5E3D4", + "reg_number": "N479PF", + "flag": "US", + "lat": 30.884375, + "lng": -93.900577, + "alt": 13850, + "dir": 117.4, + "speed": 870, + "v_speed": 0, + "flight_number": "479", + "flight_icao": "VJA479", + "dep_icao": "KOAK", + "dep_iata": "OAK", + "arr_icao": "KOPF", + "arr_iata": "OPF", + "airline_icao": "VJA", + "aircraft_icao": "CL30", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AAA244", + "reg_number": "N78438", + "flag": "US", + "lat": 29.986705, + "lng": -95.67796, + "alt": 3019, + "dir": 283.1, + "speed": 538, + "v_speed": 0, + "flight_number": "1945", + "flight_icao": "UAL1945", + "flight_iata": "UA1945", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KLAX", + "arr_iata": "LAX", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B739", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AC1A44", + "reg_number": "N8791D", + "flag": "US", + "lat": 29.550962, + "lng": -95.58617, + "alt": 3667, + "dir": 312.2, + "speed": 617, + "v_speed": 0, + "flight_number": "1944", + "flight_icao": "SWA1944", + "flight_iata": "WN1944", + "dep_icao": "KHOU", + "dep_iata": "HOU", + "arr_icao": "KABQ", + "arr_iata": "ABQ", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B38M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A30F85", + "reg_number": "N297AK", + "flag": "US", + "lat": 29.946436, + "lng": -95.381611, + "alt": 855, + "dir": 349.6, + "speed": 499, + "v_speed": 0, + "flight_number": "439", + "flight_icao": "ASA439", + "flight_iata": "AS439", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KSEA", + "arr_iata": "SEA", + "airline_icao": "ASA", + "airline_iata": "AS", + "aircraft_icao": "B39M", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AA9AFC", + "reg_number": "N7825A", + "flag": "US", + "lat": 29.914489, + "lng": -95.320061, + "alt": 3484, + "dir": 353.3, + "speed": 599, + "v_speed": 0, + "flight_number": "478", + "flight_icao": "SWA478", + "flight_iata": "WN478", + "dep_icao": "KHOU", + "dep_iata": "HOU", + "arr_icao": "KBNA", + "arr_iata": "BNA", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A05D92", + "reg_number": "N12201", + "flag": "US", + "lat": 29.993545, + "lng": -95.292064, + "alt": 193, + "dir": 270.6, + "speed": 266, + "v_speed": 0, + "flight_number": "4374", + "flight_icao": "UCA4374", + "flight_iata": "C54374", + "dep_icao": "KLRD", + "dep_iata": "LRD", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UCA", + "airline_iata": "C5", + "aircraft_icao": "E145", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A00B91", + "reg_number": "N10156", + "flag": "US", + "lat": 29.494535, + "lng": -95.285818, + "alt": 4566, + "dir": 205.1, + "speed": 674, + "v_speed": 0, + "flight_number": "4366", + "flight_icao": "UCA4366", + "flight_iata": "C54366", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KMFE", + "arr_iata": "MFE", + "airline_icao": "UCA", + "airline_iata": "C5", + "aircraft_icao": "E145", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AAF007", + "reg_number": "N80348", + "flag": "US", + "lat": 29.86838, + "lng": -95.20677, + "alt": 1404, + "dir": 124.4, + "speed": 508, + "v_speed": 0, + "flight_number": "6355", + "flight_icao": "ASH6355", + "flight_iata": "YV6355", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KSAV", + "arr_iata": "SAV", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E75L", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "0D080E", + "reg_number": "XA-GAE", + "flag": "MX", + "lat": 29.039041, + "lng": -94.757923, + "alt": 11670, + "dir": 246.1, + "speed": 778, + "v_speed": 0, + "flight_number": "2709", + "flight_icao": "SLI2709", + "flight_iata": "5D2709", + "dep_icao": "KATL", + "dep_iata": "ATL", + "arr_icao": "MMLO", + "arr_iata": "BJX", + "airline_icao": "SLI", + "airline_iata": "5D", + "aircraft_icao": "E190", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "0D0381", + "reg_number": "XA-BAC", + "flag": "MX", + "lat": 30.338517, + "lng": -97.419565, + "alt": 11991, + "dir": 42.1, + "speed": 948, + "v_speed": 0, + "flight_number": "2722", + "flight_icao": "SLI2722", + "flight_iata": "5D2722", + "dep_icao": "MMMY", + "dep_iata": "MTY", + "arr_icao": "KDTW", + "arr_iata": "DTW", + "airline_icao": "SLI", + "airline_iata": "5D", + "aircraft_icao": "E190", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AA2BA5", + "reg_number": "N75429", + "flag": "US", + "lat": 30.421841, + "lng": -96.492792, + "alt": 6131, + "dir": 83.3, + "speed": 734, + "v_speed": 0, + "flight_number": "334", + "flight_icao": "UAL334", + "flight_iata": "UA334", + "dep_icao": "KAUS", + "dep_iata": "AUS", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B739", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A63E3A", + "reg_number": "N501FX", + "flag": "US", + "lat": 30.842853, + "lng": -96.17979, + "alt": 1168, + "dir": 251.7, + "speed": 511, + "v_speed": 0, + "flight_number": "501", + "flight_icao": "LXJ501", + "dep_icao": "KTYS", + "dep_iata": "TYS", + "arr_icao": "KCLL", + "arr_iata": "CLL", + "airline_icao": "LXJ", + "aircraft_icao": "CL35", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AD83FC", + "reg_number": "N970DM", + "flag": "US", + "lat": 29.887874, + "lng": -96.068471, + "alt": 962, + "dir": 149.5, + "speed": 372, + "v_speed": 0, + "flight_number": "21", + "flight_icao": "SVL21", + "dep_icao": "KAUS", + "dep_iata": "AUS", + "arr_icao": "KSGR", + "arr_iata": "SGR", + "airline_icao": "SVL", + "aircraft_icao": "C25B", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A37FA1", + "reg_number": "N32404", + "flag": "US", + "lat": 29.993503, + "lng": -95.330138, + "alt": 94, + "dir": 272, + "speed": 305, + "v_speed": 0, + "flight_number": "2406", + "flight_icao": "UAL2406", + "flight_iata": "UA2406", + "dep_icao": "KMCO", + "dep_iata": "MCO", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B739", + "updated": 1781813792, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "ABB534", + "reg_number": "N85320", + "flag": "US", + "lat": 29.951812, + "lng": -95.343457, + "alt": 384, + "dir": 191.4, + "speed": 265, + "flight_number": "6322", + "flight_icao": "ASH6322", + "flight_iata": "YV6322", + "dep_icao": "KIAH", + "dep_iata": "IAH", + "arr_icao": "KMAF", + "arr_iata": "MAF", + "airline_icao": "ASH", + "airline_iata": "YV", + "aircraft_icao": "E170", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A1A2E0", + "reg_number": "N204WN", + "flag": "US", + "lat": 29.65377, + "lng": -95.278006, + "dir": 244.7, + "speed": 0, + "flight_number": "3709", + "flight_icao": "SWA3709", + "flight_iata": "WN3709", + "dep_icao": "KHOU", + "dep_iata": "HOU", + "arr_icao": "KTPA", + "arr_iata": "TPA", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AA9EB3", + "reg_number": "N7835A", + "flag": "US", + "lat": 29.651685, + "lng": -95.273205, + "dir": 42.2, + "speed": 11, + "flight_number": "2029", + "flight_icao": "SWA2029", + "flight_iata": "WN2029", + "dep_icao": "KHOU", + "dep_iata": "HOU", + "arr_icao": "KMCI", + "arr_iata": "MCI", + "airline_icao": "SWA", + "airline_iata": "WN", + "aircraft_icao": "B737", + "updated": 1781813793, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "AA2B79", + "reg_number": "N75410", + "flag": "US", + "lat": 30.09261, + "lng": -95.127872, + "alt": 1655, + "dir": 89, + "speed": 448, + "flight_number": "277", + "flight_icao": "UAL277", + "flight_iata": "UA277", + "dep_icao": "KLAX", + "dep_iata": "LAX", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B39M", + "updated": 1781813792, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A0D21F", + "flag": "US", + "lat": 30.076122, + "lng": -95.255892, + "alt": 10383, + "dir": 214, + "speed": 842, + "flight_number": "253", + "flight_icao": "FDX253", + "dep_icao": "KMEM", + "dep_iata": "MEM", + "arr_icao": "MMGL", + "arr_iata": "GDL", + "airline_icao": "FDX", + "airline_iata": "FX", + "aircraft_icao": "B763", + "updated": 1781813792, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A13650", + "reg_number": "N17752", + "flag": "US", + "lat": 30.010107, + "lng": -95.15097, + "alt": 1015, + "dir": 245, + "speed": 348, + "flight_number": "2165", + "flight_icao": "UAL2165", + "flight_iata": "UA2165", + "dep_icao": "KSAN", + "dep_iata": "SAN", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B39M", + "updated": 1781813792, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A2DFFE", + "reg_number": "N28457", + "flag": "US", + "lat": 30.00678, + "lng": -94.910108, + "alt": 1290, + "dir": 269, + "speed": 331, + "flight_number": "2204", + "flight_icao": "UAL2204", + "flight_iata": "UA2204", + "dep_icao": "KEWR", + "dep_iata": "EWR", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "UAL", + "airline_iata": "UA", + "aircraft_icao": "B39M", + "updated": 1781813792, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A6FFC3", + "flag": "US", + "lat": 30.192085, + "lng": -94.447061, + "alt": 9468, + "dir": 105, + "speed": 859, + "flight_number": "749", + "flight_icao": "DAL749", + "flight_iata": "DL749", + "dep_icao": "KLAX", + "dep_iata": "LAX", + "arr_icao": "KTPA", + "arr_iata": "TPA", + "airline_icao": "DAL", + "airline_iata": "DL", + "aircraft_icao": "A21N", + "updated": 1781813792, + "status": "en-route", + "type": "adsb" + }, + { + "hex": "A42323", + "reg_number": "N366FR", + "flag": "US", + "lat": 29.99342, + "lng": -95.30217, + "alt": 239, + "dir": 271, + "flight_number": "1756", + "flight_icao": "FFT1756", + "flight_iata": "F91756", + "dep_icao": "KLAX", + "dep_iata": "LAX", + "arr_icao": "KIAH", + "arr_iata": "IAH", + "airline_icao": "FFT", + "airline_iata": "F9", + "aircraft_icao": "A20N", + "updated": 1781813552, + "status": "en-route", + "type": "adsb" }, { - "hex": "A12B3C", - "reg_number": "N771UA", - "lat": 30.12, - "lng": -95.48, - "alt": 10668, - "dir": 140.0, - "speed": 740, - "v_speed": 12.7, - "flight_icao": "UAL1170", - "flight_iata": "UA1170", - "dep_icao": "KSFO", - "dep_iata": "SFO", + "hex": "A448D3", + "reg_number": "N37504", + "flag": "US", + "lat": 29.99341, + "lng": -95.161201, + "alt": 947, + "dir": 269, + "flight_number": "1036", + "flight_icao": "UAL1036", + "flight_iata": "UA1036", + "dep_icao": "MRLB", + "dep_iata": "LIR", "arr_icao": "KIAH", "arr_iata": "IAH", "airline_icao": "UAL", - "aircraft_icao": "B772", - "status": "en-route" + "airline_iata": "UA", + "aircraft_icao": "B39M", + "updated": 1781813469, + "status": "en-route", + "type": "adsb" }, { - "hex": "ABC123", - "reg_number": "N45XX", - "lat": 29.81, - "lng": -95.21, - "alt": 2438, - "dir": 10.0, - "speed": 380, - "v_speed": -5.0, - "flight_icao": "SWA45", + "hex": "AAA2B0", + "reg_number": "N7847A", + "flag": "US", + "lat": 29.65172, + "lng": -95.276649, + "dir": 335, + "flight_number": "29", + "flight_icao": "SWA29", + "flight_iata": "WN29", + "dep_icao": "KDAL", + "dep_iata": "DAL", + "arr_icao": "KHOU", + "arr_iata": "HOU", + "airline_icao": "SWA", + "airline_iata": "WN", "aircraft_icao": "B737", - "status": "en-route" + "updated": 1781813342, + "status": "en-route", + "type": "adsb" } -] +] \ No newline at end of file diff --git a/tests/test_display.py b/tests/test_display.py index 15b4e30..42678bf 100644 --- a/tests/test_display.py +++ b/tests/test_display.py @@ -120,13 +120,27 @@ def test_track_format_as_int(self) -> None: label = metrics_label(flight, page=3) assert label == "280°W" - def test_four_pages_rotates(self) -> None: + def test_five_pages_rotates(self) -> None: from jetset.display import metrics_label flight = Flight( callsign="UAL2337", altitude=35000, speed=450, vertical_rate=1500, track=270, + distance_km=42, ) - labels = [metrics_label(flight, page=i) for i in range(4)] - assert len(set(labels)) == 4 # all different + labels = [metrics_label(flight, page=i) for i in range(5)] + assert len(set(labels)) == 5 # all different + + def test_page_4_shows_distance(self) -> None: + from jetset.display import metrics_label + + flight = Flight(callsign="UAL2337", distance_km=42) + label = metrics_label(flight, page=4) + assert label == "42km away" + + def test_page_4_empty_when_no_distance(self) -> None: + from jetset.display import metrics_label + + flight = Flight(callsign="UAL2337") + assert metrics_label(flight, page=4) == "" diff --git a/tests/test_fetcher.py b/tests/test_fetcher.py index 741b8ab..2948398 100644 --- a/tests/test_fetcher.py +++ b/tests/test_fetcher.py @@ -28,12 +28,15 @@ def test_adapter_satisfies_protocol(self) -> None: AIRLABS_FLIGHT = { "flight_icao": "UAL1170", "aircraft_icao": "B772", + "lat": 30.12, + "lng": -95.48, "alt": 5559, # meters -> ~18238 ft "speed": 621, # km/h -> ~335 kn "dir": 140.0, # degrees "v_speed": 0, # m/s -> 0 ft/min "dep_iata": "SFO", "arr_iata": "LAX", + "status": "en-route", } @@ -108,6 +111,26 @@ def test_nearby_flights_handles_api_error(self) -> None: with patch.object(adapter._api, "get", return_value=mock_resp): assert adapter.nearby_flights(29.99, -95.34, 200) == [] + def test_nearby_flights_filters_out_non_enroute(self) -> None: + from jetset.fetcher import AirLabsAdapter + + adapter = AirLabsAdapter() + adapter._api_key = "test" + mock_resp = MagicMock() + mock_resp.json.return_value = { + "response": [ + dict(AIRLABS_FLIGHT), + {**AIRLABS_FLIGHT, "flight_icao": "N12345", "status": "landed"}, + {**AIRLABS_FLIGHT, "flight_icao": "N67890", "status": "scheduled"}, + ] + } + + with patch.object(adapter._api, "get", return_value=mock_resp): + flights = adapter.nearby_flights(29.99, -95.34, 200) + + assert len(flights) == 1 + assert flights[0].callsign == "UAL1170" + def test_bbox_covers_the_range(self) -> None: from jetset.fetcher import AirLabsAdapter @@ -127,12 +150,15 @@ def test_maps_all_fixture_flights(self) -> None: assert len(flights) == len(fixture) for flight in flights: assert isinstance(flight.callsign, str) + # Real API can return None for some metric fields. assert isinstance(flight.altitude, (int, type(None))) assert isinstance(flight.speed, (int, type(None))) - assert isinstance(flight.track, (float, type(None))) assert isinstance(flight.vertical_rate, (int, type(None))) - # First fixture flight has a route; the last (no dep/arr) does not. - route = flights[0].route - assert route is not None - assert route.origin.iata_code == "IAH" - assert flights[-1].route is None + # track is always cast to float in to_flight. + assert isinstance(flight.track, float) + # Real en-route flights from AirLabs all have dep/arr, + # so every fixture flight should map a route. + for flight in flights: + assert flight.route is not None, f"Flight {flight.callsign} has no route" + assert isinstance(flight.route.origin.iata_code, str) + assert isinstance(flight.route.destination.iata_code, str) diff --git a/tests/test_main.py b/tests/test_main.py index d123d4f..c6e4f92 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -89,45 +89,47 @@ class TestAppCurrentFrame: def test_cycles_window_flights_through_metric_pages(self) -> None: from jetset.app import App - app = App(AppConfig()) + app = App(AppConfig(cycles_per_flight=1)) app.buffer.set_all([Flight(callsign="UAL2337"), Flight(callsign="SWA45")]) app.last_fetch = 1000.0 frames: list[tuple[str, int]] = [] # elapsed == 0 → window_start == 0, so the window is [UAL2337, SWA45]. with patch("jetset.app.time.time", return_value=1000.0): - for frame_idx in range(8): + for frame_idx in range(10): app.frame = frame_idx result = app._current_frame() assert result is not None frames.append((result.flight.callsign, result.metric_page)) assert frames == [ - ("UAL2337", 0), ("UAL2337", 1), ("UAL2337", 2), ("UAL2337", 3), - ("SWA45", 0), ("SWA45", 1), ("SWA45", 2), ("SWA45", 3), + ("UAL2337", 0), ("UAL2337", 1), ("UAL2337", 2), ("UAL2337", 3), ("UAL2337", 4), + ("SWA45", 0), ("SWA45", 1), ("SWA45", 2), ("SWA45", 3), ("SWA45", 4), ] - def test_window_slides_over_the_refresh_interval(self) -> None: + def test_window_slides_by_one_each_cycle(self) -> None: from jetset.app import App - app = App(AppConfig()) # refresh = 2700s + app = App(AppConfig(cycles_per_flight=1)) app.buffer.set_all([Flight(callsign=str(i)) for i in range(10)]) app.last_fetch = 0.0 + + # Each cycle = PAGES_PER_FLIGHT * WINDOW_SIZE = 5 * 5 = 25 frames. + # After one cycle the window slides by 1. app.frame = 0 + frame = app._current_frame() + assert frame is not None + assert frame.flight.callsign == "0" + + app.frame = 25 + frame = app._current_frame() + assert frame is not None + assert frame.flight.callsign == "1" - # slide_interval = 2700 / 10 = 270s; one slide advances window_start by 1. - with patch("jetset.app.time.time", return_value=0.0): - frame = app._current_frame() - assert frame is not None - assert frame.flight.callsign == "0" - with patch("jetset.app.time.time", return_value=270.0): - frame = app._current_frame() - assert frame is not None - assert frame.flight.callsign == "1" - with patch("jetset.app.time.time", return_value=540.0): - frame = app._current_frame() - assert frame is not None - assert frame.flight.callsign == "2" + app.frame = 50 + frame = app._current_frame() + assert frame is not None + assert frame.flight.callsign == "2" def test_empty_buffer_returns_none(self) -> None: from jetset.app import App @@ -176,9 +178,9 @@ def test_loading_animation_cycles_with_frame(self) -> None: from jetset.app import App app = App(AppConfig()) # empty buffer - app.frame = 5 # past the first cycle — must wrap, not freeze on "LOADING" + app.frame = 6 # past the first cycle — must wrap, not freeze on "LOADING" renderer = self._run_once(app) - # 5 % 4 == 1 → "LOADING." so the dots keep animating + # 6 % 5 == 1 → "LOADING." so the dots keep animating renderer.loading.assert_called_once_with(1) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 3ddfae0..7a2bb1a 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -65,8 +65,9 @@ def test_renders_all_metric_pages_without_error(self) -> None: callsign="UAL2337", altitude=35000, speed=450, vertical_rate=1500, track=270, + distance_km=42, ) - for page in range(4): + for page in range(5): renderer.flight_card(flight, page) renderer.present()