actual times via AeroDataBox + FlightAware runway fallback
This commit is contained in:
+61
-4
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user