From 4f3be0e68e2104b66e42cac8953f5a673ba34bd2 Mon Sep 17 00:00:00 2001 From: Howard Huang Date: Thu, 18 Jun 2026 15:25:39 -0500 Subject: [PATCH 1/5] FEATURE: filter to en-route flights only and fix fixture saving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What is this change? - Add en-route status filter in AirLabsAdapter.nearby_flights so grounded flights are excluded at the data layer - Accept optional api_key in AirLabsAdapter.__init__ so scripts/save_fixtures.py can pass the key explicitly - Add load_dotenv() to save_fixtures.py so it reads AIRLABS_API_KEY from .env - Cast dir to float in to_flight (real API returns int or float) - Replace synthetic fixture with a live AirLabs capture (90 en-route flights over Houston) - Update fixture test to assert mapping contract instead of hardcoded airport codes Why is the change being made? The app was fetching all flights in range without filtering, including grounded aircraft with missing telemetry. The en-route filter gives cleaner, more useful data while staying within the API free tier. Fixture tests broke when switching to real data because they asserted specific synthetic values — they now validate the mapping contract instead. --- scripts/save_fixtures.py | 4 + src/jetset/fetcher.py | 12 +- tests/fixtures/airlabs_response.json | 2165 +++++++++++++++++++++++++- tests/test_fetcher.py | 36 +- 4 files changed, 2164 insertions(+), 53 deletions(-) diff --git a/scripts/save_fixtures.py b/scripts/save_fixtures.py index 26a1f5a..0ca885d 100644 --- a/scripts/save_fixtures.py +++ b/scripts/save_fixtures.py @@ -13,6 +13,10 @@ import sys from dataclasses import dataclass +from dotenv import load_dotenv + +load_dotenv() + from jetset.config import AppConfig from jetset.fetcher import AirLabsAdapter diff --git a/src/jetset/fetcher.py b/src/jetset/fetcher.py index 57e2699..19b44c8 100644 --- a/src/jetset/fetcher.py +++ b/src/jetset/fetcher.py @@ -43,9 +43,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: @@ -65,7 +65,7 @@ def to_flight(data: dict) -> Flight: route=route, altitude=_meters_to_feet(data.get("alt")), speed=_kmh_to_knots(data.get("speed")), - track=data.get("dir"), + track=float(data.get("dir")) if data.get("dir") is not None else None, vertical_rate=_ms_to_ft_per_min(data.get("v_speed")), ) @@ -88,7 +88,11 @@ 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" + ] if raw: return raw_flights return [self.to_flight(f) for f in raw_flights] 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_fetcher.py b/tests/test_fetcher.py index 741b8ab..e3de49b 100644 --- a/tests/test_fetcher.py +++ b/tests/test_fetcher.py @@ -34,6 +34,7 @@ def test_adapter_satisfies_protocol(self) -> None: "v_speed": 0, # m/s -> 0 ft/min "dep_iata": "SFO", "arr_iata": "LAX", + "status": "en-route", } @@ -108,6 +109,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 +148,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) From 8c90a20c74cbc352ededc3527d33d0cc48105fb1 Mon Sep 17 00:00:00 2001 From: Howard Huang Date: Thu, 18 Jun 2026 15:36:46 -0500 Subject: [PATCH 2/5] FIX: synchronise frame advance with the sliding window so flights get all metric pages before the window moves What is this change? - Replace time-based window slide (elapsed / slide_interval) with a frame-based slide that advances only after every flight in the window has displayed all PAGES_PER_FLIGHT metric pages - Update the slide test to assert on frame boundaries instead of mocking time.time at slide_interval offsets Why is the change being made? The window was sliding mid-flight-cycle because slide_interval (refresh / n, ~3.3s with 90 flights) was much shorter than the time needed to show all pages (PAGES_PER_FLIGHT * pause, 20s at 5s pause). Flights were shown for 1-2 pages then swapped out. The window now advances by 1 every frames_per_cycle frames (PAGES_PER_FLIGHT * WINDOW_SIZE), ensuring every flight gets its full page rotation before being replaced. --- src/jetset/app.py | 18 ++++++++---------- tests/test_main.py | 34 ++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/jetset/app.py b/src/jetset/app.py index cb08f7a..9cdd9e6 100644 --- a/src/jetset/app.py +++ b/src/jetset/app.py @@ -93,16 +93,14 @@ 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 + # all PAGES_PER_FLIGHT metric pages 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_cycle = self.PAGES_PER_FLIGHT * window + cycle = self.frame // frames_per_cycle + window_start = cycle % max(1, n - window + 1) if n else 0 + flight_in_window = (self.frame % frames_per_cycle) // self.PAGES_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/tests/test_main.py b/tests/test_main.py index d123d4f..01d97c6 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -107,27 +107,29 @@ def test_cycles_window_flights_through_metric_pages(self) -> None: ("SWA45", 0), ("SWA45", 1), ("SWA45", 2), ("SWA45", 3), ] - 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()) app.buffer.set_all([Flight(callsign=str(i)) for i in range(10)]) app.last_fetch = 0.0 - app.frame = 0 - # 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" + # Each cycle = PAGES_PER_FLIGHT * WINDOW_SIZE = 4 * 5 = 20 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 = 20 + frame = app._current_frame() + assert frame is not None + assert frame.flight.callsign == "1" + + app.frame = 40 + 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 From cad847ee9f12a0ab8b9bf04915b00b2f0137912a Mon Sep 17 00:00:00 2001 From: Howard Huang Date: Thu, 18 Jun 2026 15:39:33 -0500 Subject: [PATCH 3/5] FEATURE: make metric page cycles per flight configurable What is this change? - Add cycles_per_flight (default 1) to AppConfig - Use it in _current_frame() so each flight repeats its metric page cycle that many times before the window advances Why is the change being made? With pause=5s and 4 metric pages, each flight gets 20s of screen time. Some users may want more (or less) time per flight without changing the per-page pause. cycles_per_flight=2 gives 40s per flight (all 4 pages shown twice) without altering the pause or the window slide cadence. --- src/jetset/app.py | 14 ++++++++------ src/jetset/config.py | 11 ++++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/jetset/app.py b/src/jetset/app.py index 9cdd9e6..325caf6 100644 --- a/src/jetset/app.py +++ b/src/jetset/app.py @@ -94,13 +94,15 @@ def _current_frame(self) -> Frame | None: window = min(self.WINDOW_SIZE, n) # Show a sliding window of flights, one at a time. Each flight gets - # all PAGES_PER_FLIGHT metric pages 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_cycle = self.PAGES_PER_FLIGHT * window - cycle = self.frame // frames_per_cycle + # 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_cycle) // self.PAGES_PER_FLIGHT + 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 From 4b437313d313ae0676db38c424abfb9480395539 Mon Sep 17 00:00:00 2001 From: Howard Huang Date: Thu, 18 Jun 2026 15:42:38 -0500 Subject: [PATCH 4/5] FEATURE: sort fetched flights by distance to home, closest first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What is this change? - Add _haversine_km helper for great-circle distance - Sort raw flights by proximity to home in nearby_flights before converting to Flight objects - Add lat/lng to the AIRLABS_FLIGHT test fixture - Fix two frame-cycle tests to explicitly set cycles_per_flight=1 so they aren't sensitive to the config default Why is the change being made? Without sorting, flights appear in API response order (typically by ICAO hex). The display now shows the closest flights first, which is more useful on a 5-flight sliding-window display — the nearest traffic is the most relevant. --- src/jetset/fetcher.py | 21 +++++++++++++++++++++ tests/test_fetcher.py | 2 ++ tests/test_main.py | 4 ++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/jetset/fetcher.py b/src/jetset/fetcher.py index 19b44c8..72c2239 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. @@ -93,6 +107,13 @@ def nearby_flights( 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] diff --git a/tests/test_fetcher.py b/tests/test_fetcher.py index e3de49b..2948398 100644 --- a/tests/test_fetcher.py +++ b/tests/test_fetcher.py @@ -28,6 +28,8 @@ 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 diff --git a/tests/test_main.py b/tests/test_main.py index 01d97c6..b4b2e32 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -89,7 +89,7 @@ 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 @@ -110,7 +110,7 @@ def test_cycles_window_flights_through_metric_pages(self) -> None: def test_window_slides_by_one_each_cycle(self) -> None: from jetset.app import App - app = App(AppConfig()) + 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 From 278d14fc46ab31720e736a51457b24ac87c27232 Mon Sep 17 00:00:00 2001 From: Howard Huang Date: Thu, 18 Jun 2026 21:06:38 -0500 Subject: [PATCH 5/5] FEATURE: add distance-from-home metric as a 5th metric page What is this change? - Add distance_km field to Flight model - Add haversine calculation in to_flight, passed home coordinates from nearby_flights - Add page 4 to metrics_label showing rounded distance in km - Increase PAGES_PER_FLIGHT from 4 to 5 so distance is shown alongside altitude, speed, vertical rate, and track Why is the change being made? Knowing how far each flight is from the home location is useful context on the display. The distance is calculated in the adapter from the raw API coordinates and home position, so the model stays clean and the metric is available to any page without recomputing. --- scripts/save_fixtures.py | 4 ++-- src/jetset/app.py | 2 +- src/jetset/display.py | 7 +++++-- src/jetset/fetcher.py | 22 +++++++++++++++++----- src/jetset/models.py | 1 + tests/test_display.py | 20 +++++++++++++++++--- tests/test_main.py | 16 ++++++++-------- tests/test_renderer.py | 3 ++- 8 files changed, 53 insertions(+), 22 deletions(-) diff --git a/scripts/save_fixtures.py b/scripts/save_fixtures.py index 0ca885d..c966998 100644 --- a/scripts/save_fixtures.py +++ b/scripts/save_fixtures.py @@ -15,11 +15,11 @@ from dotenv import load_dotenv -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 325caf6..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 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 72c2239..bcc180b 100644 --- a/src/jetset/fetcher.py +++ b/src/jetset/fetcher.py @@ -35,7 +35,7 @@ def _ms_to_ft_per_min(ms: float | None) -> int | None: def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float: """Great-circle distance between two points in km.""" - R = 6371 + r = 6371 dlat = math.radians(lat2 - lat1) dlon = math.radians(lon2 - lon1) a = ( @@ -44,7 +44,7 @@ def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float: * math.cos(math.radians(lat2)) * math.sin(dlon / 2) ** 2 ) - return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + return r * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) class AirLabsAdapter(FlightAPI): @@ -69,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=float(data.get("dir")) if data.get("dir") is not None else None, + 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( @@ -116,7 +128,7 @@ def nearby_flights( ) 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/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_main.py b/tests/test_main.py index b4b2e32..c6e4f92 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -96,15 +96,15 @@ def test_cycles_window_flights_through_metric_pages(self) -> None: 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_by_one_each_cycle(self) -> None: @@ -114,19 +114,19 @@ def test_window_slides_by_one_each_cycle(self) -> None: 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 = 4 * 5 = 20 frames. + # 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 = 20 + app.frame = 25 frame = app._current_frame() assert frame is not None assert frame.flight.callsign == "1" - app.frame = 40 + app.frame = 50 frame = app._current_frame() assert frame is not None assert frame.flight.callsign == "2" @@ -178,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()