[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:[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
======