Parsing ACARS OOOI Messages into Duty Events
The exact task this guide solves is turning the stream of ACARS OOOI movement messages — Out of the gate, Off the ground, On the ground, In at the gate — into the validated, UTC-normalised duty events a compliance engine reasons about, and deriving block and flight time from them. OOOI reports are the ground truth for when an aircraft actually moved, which makes them the authoritative source for block-to-block and airborne durations, but they arrive as terse, out-of-order text that must be parsed, ordered, and validated before any duration is trustworthy. This page parses the four events, reconstructs the flight leg, and derives the times. It is part of the flight log parsing pipelines section and produces the segment boundaries the duty time validation rule engines consume.
Prerequisites
- Python 3.11+ with typed models.
- A raw ACARS OOOI feed — messages tagged with a flight id, event type, and timestamp.
- A tail-to-fleet mapping so events can be grouped by aircraft and leg.
- UTC normalisation — OOOI timestamps carry an offset and are stored in UTC.
The four events and what they bound
An OOOI sequence records four instants for a leg. Out is push-back from the gate, which starts block time. Off is wheels-up, which starts flight time. On is wheels-down, which ends flight time. In is arrival at the gate, which ends block time. Two durations fall straight out of them: block time is In minus Out, and flight (airborne) time is On minus Off. The parser’s job is to group the four messages into a leg, confirm they are complete and ordered, and derive the two durations — because a missing or out-of-order event silently corrupts every downstream calculation that trusts the block boundary.
Step 1 — Parse a raw message into a typed event
Each message is parsed into a typed event with a validated timestamp, rejecting anything without an offset at the boundary. An enum fixes the four legal event types so a typo cannot introduce a fifth.
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum
class OooiKind(Enum):
OUT = "OUT"
OFF = "OFF"
ON = "ON"
IN = "IN"
@dataclass(frozen=True, slots=True)
class OooiEvent:
flight_id: str
kind: OooiKind
instant_utc: datetime
def parse_message(flight_id: str, kind: str, raw_ts: str) -> OooiEvent:
parsed = datetime.fromisoformat(raw_ts)
if parsed.tzinfo is None:
raise ValueError(f"naive OOOI timestamp rejected: {raw_ts!r}")
return OooiEvent(flight_id, OooiKind(kind), parsed.astimezone(timezone.utc))
Verify: parse_message("UA123", "OFF", "2026-07-17T14:05:00-04:00") yields an OFF event at 18:05Z; a message with kind "PUSH" raises a ValueError from the enum, and a naive timestamp raises at the offset check.
Step 2 — Assemble a complete, ordered leg
The four events for a leg must all be present and satisfy Out ≤ Off ≤ On ≤ In. Grouping by flight id and validating the ordering catches the missing and misordered events that are the common corruption in an OOOI feed.
def assemble_leg(events: list[OooiEvent]) -> dict:
by_kind = {e.kind: e for e in events}
missing = [k.value for k in OooiKind if k not in by_kind]
if missing:
raise ValueError(f"incomplete OOOI leg, missing {missing}")
out, off, on, inn = (by_kind[k].instant_utc for k in
(OooiKind.OUT, OooiKind.OFF, OooiKind.ON, OooiKind.IN))
if not out <= off <= on <= inn:
raise ValueError("OOOI events out of order: expected OUT ≤ OFF ≤ ON ≤ IN")
return {"out": out, "off": off, "on": on, "in": inn}
Verify: four in-order events assemble into a leg dict; a leg missing its IN message raises with missing=['IN']; a leg whose ON precedes its OFF raises the ordering error rather than producing a negative flight time.
Step 3 — Derive block and flight time
With an ordered leg, the two durations are simple subtractions of UTC instants — correct across any DST or date-line boundary because the instants are absolute.
def leg_durations(leg: dict) -> dict:
block_minutes = (leg["in"] - leg["out"]).total_seconds() / 60.0
flight_minutes = (leg["on"] - leg["off"]).total_seconds() / 60.0
return {
"block_minutes": round(block_minutes, 1),
"flight_minutes": round(flight_minutes, 1),
"taxi_minutes": round(block_minutes - flight_minutes, 1),
}
Verify: a leg with Out 18:00Z, Off 18:20Z, On 20:50Z, In 21:00Z yields 180 block minutes, 150 flight minutes, and 30 taxi minutes — and the taxi figure equals block minus flight, a useful cross-check that the four events are internally consistent.
Failure modes and troubleshooting
- Missing event. A leg missing its
INcannot bound block time. Remediation: require all four events before deriving durations; quarantine incomplete legs. - Out-of-order events. An
ONbeforeOFFyields a negative flight time. Remediation: validate the Out ≤ Off ≤ On ≤ In ordering and reject violations. - Naive timestamps. An OOOI time without an offset drifts across DST. Remediation: reject naive timestamps at parse time.
- Duplicate reports. ACARS can re-transmit an event, producing two
OFFmessages. Remediation: deduplicate by flight id and kind, keeping the authoritative report. - Wrong leg grouping. Grouping by tail alone merges consecutive legs. Remediation: group by flight id and leg, and confirm the taxi figure is non-negative as a consistency check.
Frequently Asked Questions
Why are OOOI events the authoritative source for block and flight time?
Because they record when the aircraft physically moved — push-back, wheels-up, wheels-down, gate arrival — rather than a scheduled or estimated time. Block and flight time derived from actual OOOI instants are what a regulator and the duty-time engine treat as ground truth.
What is the difference between block time and flight time?
Block time runs gate to gate — Out to In — and includes taxi at both ends. Flight time runs wheels-up to wheels-down — Off to On — and is the airborne portion only. Their difference is total taxi time, which is a useful consistency check on the leg.
How are out-of-order or missing events handled?
A leg must have all four events in the order Out ≤ Off ≤ On ≤ In. A missing event or a violated ordering means the leg cannot be trusted, so it is quarantined for review rather than producing a corrupt duration.
Why deduplicate OOOI messages?
Because ACARS may re-transmit the same event, and two OFF messages for one leg would otherwise overwrite or duplicate the airborne boundary. Deduplicating by flight id and event kind keeps one authoritative instant per event.
Related
- Flight Log Parsing Pipelines — the parent section and parsing patterns.
- Detecting Overlapping Duty Segments in Python — validating the legs these events produce.
- Handling UTC and DST Edge Cases in Flight Time Math — why the durations are computed in UTC.
- Flight Data Ingestion & System Sync — the pipeline these events feed.
Back to Flight Log Parsing Pipelines.