[Keith Vetter] 2019-10-04 -- https://opensky-network.org/%|%OpenSky Network%|% is a *community-based receiver network which continually collects air traffic surveillance data.* It has a https://opensky-network.org/apidoc/%|%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. https://flightaware.com/%|%FlightAware%|% (of the https://github.com/flightaware/Tcl-bounties%|%tcl-bounty program%|%) offers similar service but for a https://flightaware.com/commercial/flightxml/pricing_class.rvt%|%fee%|%. Likewise there is https://planefinder.net/%|%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 https://opensky-network.org/apidoc/python.html%|%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:PASSWORD@opensky-network.org/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 ======