actual times via AeroDataBox + FlightAware runway fallback

This commit is contained in:
2026-09-12 12:40:41 +10:00
parent c2e78cb256
commit d524e6d343
8 changed files with 698 additions and 5 deletions
+61 -4
View File
@@ -11,8 +11,9 @@ review page's "re-query" button - same function, so a corrected field
import logging
import threading
import time
from datetime import date, datetime, timedelta
from datetime import date, datetime, timedelta, timezone
import aerodatabox
import config
import flightaware
import models
@@ -34,6 +35,41 @@ def next_run_datetime(hhmm: str, now: datetime | None = None) -> datetime:
return candidate
def _iso_from_unix(ts: int) -> str:
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec="seconds")
def _fetch_gate_times(flight_number: str, flight_date: date, updates: dict) -> None:
"""Best-effort AeroDataBox lookup for scheduled/actual GATE times
(departure/arrival) - the data BCBP and the FlightAware track can't
provide. Failures here are recorded (schedule_error) but never block
the FlightAware track or push status to track_unavailable: worst case
the gate fields stay blank exactly like they do today, and the
existing fallback (AirTrail's own manual Search) still works."""
try:
gate_times = aerodatabox.lookup_gate_times(flight_number, flight_date)
except aerodatabox.AeroDataBoxError as e:
updates["schedule_error"] = str(e)
return
updates["schedule_error"] = None
if not gate_times:
return
updates["gate_departure_scheduled"] = gate_times.get("departureScheduled")
updates["gate_departure_actual"] = gate_times.get("departure")
updates["gate_arrival_scheduled"] = gate_times.get("arrivalScheduled")
updates["gate_arrival_actual"] = gate_times.get("arrival")
# AeroDataBox's runwayTime is an authoritative actual takeoff/landing
# time - prefer it. Only set here if present; the FlightAware-track
# fallback below fills in behind it (checks "not in updates") rather
# than overwriting it unconditionally.
if gate_times.get("takeoffActual"):
updates["takeoff_actual"] = gate_times["takeoffActual"]
if gate_times.get("landingActual"):
updates["landing_actual"] = gate_times["landingActual"]
def process_one_flight(row: dict) -> None:
flight_id = row["id"]
@@ -45,6 +81,19 @@ def process_one_flight(row: dict) -> None:
updates = {"from_icao": from_icao, "to_icao": to_icao, "operating_carrier_icao": carrier_icao}
try:
flight_date = date.fromisoformat(row["flight_date"])
except (TypeError, ValueError):
flight_date = None
# Gate times only need a flight number + date (no ICAO resolution), so
# attempt this regardless of whether the airport/carrier lookups below
# succeed - an unresolved airport shouldn't cost us data we could
# otherwise have gotten.
iata_flight_number = f"{row.get('operating_carrier_iata') or ''}{row.get('flight_number') or ''}"
if iata_flight_number.strip() and flight_date:
_fetch_gate_times(iata_flight_number, flight_date, updates)
if not from_icao or not to_icao or not carrier_icao:
missing = [
name
@@ -57,9 +106,7 @@ def process_one_flight(row: dict) -> None:
notifier.notify_flight_staged(models.get_flight(flight_id))
return
try:
flight_date = date.fromisoformat(row["flight_date"])
except (TypeError, ValueError):
if flight_date is None:
updates["status"] = "track_unavailable"
updates["error_message"] = f"invalid flight_date: {row.get('flight_date')!r}"
models.update_flight(flight_id, **updates)
@@ -98,6 +145,16 @@ def process_one_flight(row: dict) -> None:
updates["track_times"] = times
updates["track_point_count"] = len(coordinates)
updates["error_message"] = None
# The ADS-B track only exists while the aircraft is squawking -
# roughly wheels-up to touchdown - so its first/last timestamps are
# a decent proxy for actual takeoff/landing when AeroDataBox didn't
# already give us its authoritative runwayTime above (no key
# configured, lookup failed, or that particular flight had no
# runwayTime in its response). Never overwrite a value AeroDataBox
# already set - that one's more precise.
if times:
updates.setdefault("takeoff_actual", _iso_from_unix(times[0]))
updates.setdefault("landing_actual", _iso_from_unix(times[-1]))
models.update_flight(flight_id, **updates)
notifier.notify_flight_staged(models.get_flight(flight_id))