OpenSky API

Keith Vetter 2019-10-04 -- OpenSky Network is a community-based receiver network which continually collects air traffic surveillance data. It has a public API that lets you retrieve live airspace information for research and non-commercial purposes. The free API is restricted to only the current information and has a rate limit of once every 10 seconds.

FlightAware (of the tcl-bounty program ) offers similar service but for a fee . Likewise there is PlaneFinder and again they charge to access their API.

This is a tcl module that provides a front-end to OpenSky REST API. It mimics their python module . It allows you to retrieve information on all flights currently in the air, or just a single flight, or only flights in a certain range of latitudes and longitudes.

##+##########################################################################
#
# OpenSkyApi -- front end to the OpenSky Network REST api letting you
# retrieve live airspace information for research and non-commercial purposes.
# Note, this API enforces the rate limit this site has.
#
# Usage:
#   set stateVector[::OpenSky::GetStates ?options...?
#     -time ####      The time which the state vectors are associated with.
#                     Ignored if unauthenticated
#     -icao24 acd640  Retrieve states for the given ICAO24 addresses
#     -bbox min_lat max_lat min_lon max_lon
#                     Only retrieve states with a bounding box, specified by
#     -username ...   OpenSky username if you have an account
#     -password ...   OpenSky password if you have an account
#     -wait           If set then automatically wait until OpenSky's rate limit has
#                     expired: 10 seconds for unauthenticated users, 2 otherwise
#     -verbose        If set then print information about what's happening
#     -nofetch        Don't fetch any data, just return url
#
# The home page is available at https://opensky-network.org/apidoc/index.html
# The REST api is documented at https://opensky-network.org/apidoc/rest.html
# The official Python API is at https://github.com/openskynetwork/opensky-api
#
# by Keith Vetter 2019-09-25
#
# A state vector consists of:
#                          Index Property        Type    Description
#       "ac96b8",          0     icao24          string  Unique ICAO 24-bit address of the transponder in hex string representation.
#       "AAL1042 ",        1     callsign        string  Callsign of the vehicle (8 chars). Can be null if no callsign has been received.
#       "United States",   2     origin_country  string  Country name inferred from the ICAO 24-bit address.
#       1526690760,        3     time_position   int     Unix timestamp (seconds) for the last position update.
#       1526690760,        4     last_contact    int     Unix timestamp (seconds) for the last update in general.
#       -77.226,           5     longitude       float   WGS-84 longitude in decimal degrees.
#       35.5134,           6     latitude        float   WGS-84 latitude in decimal degrees.
#       9685.02,           7     geo_altitude    float   Geometric altitude in meters.
#       false,             8     on_ground       boolean Boolean value which indicates if the position was retrieved from a surface position report.
#       219.47,            9     velocity        float   Velocity over ground in m/s.
#       232.62,            10    heading         float   Heading in decimal degrees clockwise from north (i.e. north=0°).
#       -4.88,             11    vertical_rate   float   Vertical rate in m/, positive for climbing, negative for descending.
#       null,              12    sensors         int[]   IDs of the receivers which contributed to this state vector.
#       10187.94,          13    baro_altitude   float   Barometric altitude in meters.
#       "3517",            14    squawk          string  The transponder code aka Squawk.
#       false,             15    spi             boolean Whether flight status indicates special purpose indicator.
#       0                  16    position_source int     Origin of this state’s position: 0 = ADS-B, 1 = ASTERIX, 2 = MLAT
#

package provide OpenSky 0.9

package require json
package require http
package require tls
http::register https 443 [list ::tls::socket -tls1 1]


namespace eval OpenSky {
    variable api_protocol "https://"
    variable api_url "opensky-network.org/api/states/all"
    variable rate_limit_auth 2
    variable rate_limit_noauth 10
    variable last_request 0
    variable has_auth False
    variable verbose False
    variable CONFIG [dict create {*}{
        -time ""
        -icao24 {}
        -bbox {}
        -username ""
        -password ""
        -wait False
        -verbose False
        -nofetch False
    }]
    variable url ""
    variable msg "set state \[::OpenSky::GetStates ?options...?\]\n"
    append msg "  -time     The time which the state vectors are associated with\n"
    append msg "            ignored if not authenticated\n"
    append msg "  -icao24   Optionally retrieve states for the given ICAO24 addresses\n"
    append msg "  -bbox     Optionally only retrieve states with a bounding box, specified by\n"
    append msg "            four values: min_lat max_lat min_lon max_lon\n"
    append msg "  -username OpenSky username if you have an account\n"
    append msg "  -password OpenSky password if you have an account\n"
    append msg "  -wait     If set then automatically wait until OpenSky's rate limit has\n"
    append msg "            expired: 10 seconds for unauthenticated users, 2 otherwise\n"
    append msg "  -verbose  If set then print information about what's happening\n"
    append msg "  -nofetch  Don't fetch any data, just return url"
}

proc ::OpenSky::GetStates {args} {
    # Fetches the OpenSky state vectors for the given parameters.
    # Result is a dictionary of JSON data with three keys:
    #   time  : The time which the state vectors in this response are
    #           associated with. All vectors represent the state of a vehicle
    #           with the interval [time−1,time].
    #   states: A list of state vectors
    #   url   : The OpenSky API url for the REST api
    #

    lassign [::OpenSky::_GetStates {*}$args] emsg states
    if {$emsg ne ""} { error $emsg }
    return $states
}

proc ::OpenSky::_GetStates {args} {
    # Entry point which doesn't throw an error on bad data
    variable last_request

    lassign [::OpenSky::_ParseArgs $args] emsg config
    if {$emsg ne ""} { return [list $emsg {}] }

    set url [::OpenSky::_MakeUrl $config]
    if {[dict get $config -nofetch]} {
        return [list "" [dict create url $url time [clock seconds] states null]]
    }
    if {[::OpenSky::_AreWeRateLimited [dict get $config -wait]]} {
        return [list "Blocking request due to rate limit" {}]
    }
    ::OpenSky::_log "Fetching url: $url"
    set start [clock seconds]
    set token [::http::geturl $url]
    set duration [expr {[clock seconds] - $start}]
    set ncode [::http::ncode $token]
    set data [::http::data $token] ; list
    ::http::cleanup $token
    ::OpenSky::_log [format "Got response: %d: %s in %s" $ncode \
                         [::OpenSky::_Plural [string length $data] byte] \
                         [::OpenSky::_Plural $duration second]]

    if {$ncode != 200} {
        return [list "bad response from opensky: $ncode" {}]
    }
    set last_request [clock seconds]
    set jdata [::json::json2dict $data]
    set jdata [dict merge [dict create url $url] $jdata]
    return [list "" $jdata]
}
proc ::OpenSky::_log {msg} {
    if {$::OpenSky::verbose} {
        puts stderr $msg
    }
}
proc ::OpenSky::_Plural {cnt singular {plural ""}} {
    if {$cnt == 1} { return [string trim "$cnt $singular"] }

    set num $cnt
    while {[regsub {^([-+]?[0-9]+)([0-9][0-9][0-9])} $num {\1,\2} num]} {}

    if {$plural eq "" && $singular ne ""} { set plural "${singular}s" }
    return [string trim "$num $plural"]
}
proc ::OpenSky::_MakeUrl {config} {
    variable api_protocol
    variable api_url
    variable has_auth
    variable url

    # https://opensky-network.org/api/states/all
    # https://USERNAME:[email protected]/api/states/all
    # https://opensky-network.org/api/states/all?icao24=3c6444&icao24=3e1bf9

    set auth ""
    if {$has_auth} {
        set auth "[dict get $config -username]:[dict get $config -password]@"
    }

    set params {}
    if {[dict get $config -time] ne ""} {
        lappend params "time=[dict get $config time]"
    }
    if {[dict get $config -icao24] ne {}} {
        lappend params "icao24=[join [dict get $config -icao24] &icao24=]"
    }
    if {[dict get $config -bbox] ne {}} {
        foreach key {lamin lomin lamax lomax} value [dict get $config -bbox] {
            lappend params "$key=$value"
        }
    }
    set url "$api_protocol$auth$api_url"
    if {$params ne {}} {
        append url "?" [join $params "&"]
    }
    ::OpenSky::_log "Making url: $url"
    return $url
}
proc ::OpenSky::_AreWeRateLimited {sleepUntilOk} {
    variable rate_limit_auth
    variable rate_limit_noauth
    variable last_request
    variable has_auth

    set delta [expr {[clock seconds] - $last_request}]
    set limit [expr {$has_auth ? $rate_limit_auth : $rate_limit_noauth}]
    if {$delta < $limit && ! $sleepUntilOk} { return True }
    if {$delta < $limit && $sleepUntilOk} {
        set seconds [expr {$limit - $delta}]
        ::OpenSky::_log "Rate limited: sleeping $seconds seconds..."
        after [expr {$seconds * 1000}]
    }
    return False
}
proc ::OpenSky::_ParseArgs {arg_list} {
    variable CONFIG
    variable msg
    variable has_auth
    variable verbose

    set config [dict merge $CONFIG]
    for {set i 0} {$i < [llength $arg_list]} {} {
        set key [lindex $arg_list $i]

        if {$key in {-wait -verbose -nofetch}} {
            dict set config $key True
            incr i
        } elseif {$key in {-time -username -password}} {
            dict set config $key [lindex $arg_list $i+1]
            if {$i + 2 > [llength $arg_list]} { return [list "not enough arguments for $key" {}]}
            incr i 2
        } elseif {$key eq "-bbox"} {
            if {$i + 5 > [llength $arg_list]} { return [list "not enough arguments for $key" {}]}
            dict set config $key [lrange $arg_list $i+1 $i+4]
            incr i 5
            ::OpenSky::_log "Retrieving for bounding box [dict get $config $key]"
        } elseif {$key eq "-icao24"} {
            set values {}
            for {set i [expr {$i + 1}]} {$i < [llength $arg_list]} {incr i} {
                set value [lindex $arg_list $i]
                if {[string index $value 0] eq "-"} break
                lappend values $value
            }
            dict set config $key $values
            ::OpenSky::_log "Retrieving for icao24 matching $values"
        } else {
            return [list "unknown option '$key'\nusage: $msg" {}]
        }
    }
    set has_auth [expr {[dict get $config -username] ne ""}]
    set verbose [dict get $config -verbose]
    return [list "" $config]
}

# Demo code that only runs when the script is directly invoked
if { [file tail [info script]] == [file tail $::argv0] } {
    set reply [::OpenSky::GetStates -verbose]
    set states [dict get $reply states]
    set cnt [llength $states]
    set when [clock format [dict get $reply time]]
    puts "\n\nFor $when, found info for [::OpenSky::_Plural $cnt plane]"

    unset -nocomplain countries
    foreach state $states {
        incr countries([lindex $state 2])
    }
    set top10 [lrange [lsort -stride 2 -index 1 -integer -decreasing [array get countries]] 0 19]
    set maxl [::tcl::mathfunc::max {*}[lmap {k v} $top10 {string length $k}]]

    puts "Top 10 countries with most originating flights:"
    foreach {country count} $top10 {
        puts [format "  %-*s   = %5s" $maxl $country [::OpenSky::_Plural $count ""]]
    }
    puts ""

    set icaos [lmap s [lrange $states 0 3] {lindex $s 0}]
    set reply2 [::OpenSky::GetStates -wait -verbose -icao24 {*}$icaos]
    set states2 [dict get $reply2 states]

    puts "\nDetails for [llength $icaos] flights"
    foreach state $states2 {
        lassign $state icao24 . country . . lon lat alt . velocity
        puts [string cat "  Flight $icao24 is at $lat $lon elevation ${alt} m " \
              "moving at $velocity m/s coming from $country"]
    }
    puts ""

    # Example query with bounding box covering Switzerland:
    set reply3 [::OpenSky::GetStates -verbose -wait -bbox 45.8389 5.9962 47.8229 10.5226]
    set states [dict get $reply3 states]
    set cnt [llength $states]
    set when [clock format [dict get $reply3 time]]
    puts "\nAt $when, found [::OpenSky::_Plural $cnt plane] flying over Switzerland"
}
return