So I had some work to do and I found a few more things to change. I had tree problems to overcome.
1. As mentioned earlier, it appears that certain fields are not always filled or contain a string where a numerical value is expected and used for calculations. This is now being captured as much as possible. A variable can be set now to give these fields the value 'unknown'.
I have not seen that the record cannot be written because of a nil field after this change.
I discovered a new way of declaring a variable where a second option is used when the field is nil. See the
.
2. Flights were always inserted and updated in the table and were only written when no more flights were seen.
If the search radius and altitude are set very large, or if there is a lot of air traffic within the search area, so many planes could pass by that the script did not have time to write it. For that reason I have changed the order of some actions. That was not easy, because it has grown to be a huge script with consecutive tasks. That is why I have now moved some tasks to separate functions, which also makes it easier to read.
3. But now I had to find out when a plane is ready to be written and removed from the table.
If an aircraft is seen multiple times, the row in the table is updated if it came closer. If it is further, the row will be marked as 'leaving' as a sign to write. That works well, but is a problem for an aircraft that (fast passing or small search area) is only seen once, or is no longer seen once it travels further. So it will never be marked as 'leaving'.
A variable 'ap_forcewriteCounter' has now been set for this purpose. When this number is reached, the row will be written and deleted. The number is debatable. I've set it to 10 now. The last comment here is that it can lead to the situation that an aircraft, that was seen earlier, is being written after others. However, the time that is was seen is in the text and in the CSV file.
I have added field descriptions to the example fields of the result_table. Moved this as a reference to the end of the script.
I tested this with large numbers by increasing the search area so that Schiphol airport was within the radius and it seems to work well.
NB. The used table and functions placed in global_data script (mentioned earlier) are still needed, but remain unchanged.
Code: Select all
-- 24-05-2024 script by Jan Peppink, https://ict.peppink.nl
-- Goal: Aircraft 'noise pollution detector'.
-- https://api.airplanes.live/v2/point/<lat>/<long>/<distanceKM>
-- Get data of airplaines that pass close by with lower altitudes.
-- When you hear them, they are probably logged. :-)
-- 27-05-2024 Call every 15 seconds. Store playe in global data table.
-- Update this when same plane is closer with call
-- When no planes are catched write the table (if not empty) to device.
-- 29-05-2024 alt_geom is sometimes empty. If so, take alt_baro. When this is also empty then set as 'unkown'.
-- 29-05-2024 Added optional saving to tab delimited csv file, with fieldnames as header.
--- Makes use of function 'AppendStringToFile' in global_data.
-- 31-05-2024 Added ap_pollFreq for setting the poll interval in seconds per 5 minutes.
-- 24-06-2024 Added optional incremental counter device 'ap_counter_idx'.
-- 28-06-2024 Added the date to the csv file export.
-- 10-07-2024 Prevent nil or non numeric values that can not be read or calculated.
-- Added " or ap_unknown --If variable is nil, 'or' will evaluate to the second operand.
-- Added field descriptions to the Example fields. Moved this as a reference to the end of the script.
-- 13-07-2024 Completely new code layout with separate functions.
-- Deleting rows from table now no longer waits until no more flights are seen. (could take to long).
-- A fast passer-by in a small area may be catched only once and not seen as leaving.
-- If flight is still found (with ap_forcewriteCounter) in the loop after arbitrary xx times, it is written.
--- Your settings ----------------------------------------------------------------
-- First set the used device index numbers and variables you might want to change.
---#################################################################
local ap_text_idx = 99999 --idx of the custom Text device
local ap_counter_idx = 99999 --idx of the optional incremental counter device. (or 99999 if not confugured)
local ap_maxRadiusKm = 3 -- Query max radius in KM around your coordinates.
local ap_maxAltitudeM = 2500 -- Save only < max ap_maxAltitude in Meters.
local ap_useCSVfile = 1 -- 0 = NO csv file, or 1 = Save airplanes also in csv file.
local ap_csvFile = '/<pathtoyourfile>/SavedAirplanes.csv'
local ap_pollFreq = 15 --Poll frequncy in seconds. Default 15. Normally > 1 and < 60.
local ap_unknown = 'onbekend' --Translate to mark nil values as unknown.
local ap_forcewriteCounter = 10 --Force to write and remove from table when still seen in the loop x times.
----------------------------------------------------------------------------------
return {
on = {
timer = {
'every 5 minutes', -->>>KEEP THIS at 5 minutes!<<<--Adjust ap_pollFreq in seconds in Your settings.
},
httpResponses = {
'airplanes' -- matches callback string below
},
},
logging = {
-- Level can be domoticz.LOG_INFO, domoicz.LOG_MODULE_EXEC_INFO, domoticz.LOG_DEBUG, domoticz.LOG_ERROR or domoticz.LOG_FORCE
level = domoticz.LOG_INFO,
--level = domoticz.LOG_DEBUG,
marker = 'Airplanes-',
},
execute = function(dz, triggeredItem)
-- Set Local environment=================
local _u = dz.utils -- Holds subset of handy utilities.
local _h = dz.helpers -- Holds the global functions
local _d = dz.globalData -- Holds the global data
--_d.initialize('ap_table') -- Re-initialize the table
-- Set location coordinate and distance variables.
local lat = dz.settings.location.latitude
local long = dz.settings.location.longitude
local ap_maxRadiusMiles = ap_maxRadiusKm * 0.62137119 -- Calculate given km in sdettings to miles.
--Declare for airplane item variables.
local result_table
local ap_date
local ap_time
local ap_hex
local ap_flight
local ap_r
local ap_t
local ap_desc
local ap_gs
local ap_track
local ap_direction
local ap_lat
local ap_long
local ap_distance
local ap_geom_rate
local ap_geom_rateString
local ap_altitude
-- Local Functions go here =============
local function airplanesExist( testValue )
-- check if this plane is already in _d.ap_table
if type( _d.ap_table ) == "table" then
for i = 1, #_d.ap_table do
if _d.ap_table[i].ap_hex == testValue then
-- Found in table
dz.log( 'Flight ' .. testValue .. ' is found in _d.ap_table', dz.LOG_DEBUG )
return true
end
end
-- Not found in table
dz.log( 'Flight ' .. testValue .. ' is not found in _d.ap_table', dz.LOG_DEBUG )
return false
else
-- Table not found.
dz.log( 'I did not find _d.ap_table - Re-initialize.', dz.LOG_DEBUG )
_d.initialize('airplanes') -- Re-initialize the table
return false
end
end
local function insertinAPtable()
table.insert( _d.ap_table, {
ap_date = ap_date,
ap_time = ap_time,
ap_hex = ap_hex,
ap_flight = ap_flight,
ap_r = ap_r,
ap_t = ap_t,
ap_desc = ap_desc,
ap_gs = ap_gs,
ap_track = ap_track,
ap_direction = ap_direction,
ap_lat = ap_lat,
ap_long = ap_long,
ap_distance = ap_distance,
ap_geom_rate = ap_geom_rate,
ap_geom_rateString = ap_geom_rateString,
ap_altitude = ap_altitude,
ap_scriptstatus = 'firstseen',
ap_scriptcount = 1
});
dz.log( 'Created new row for flight ' .. ap_hex .. ' in _d.ap_table with distance ' .. ap_distance .. ' km.', dz.LOG_DEBUG )
end
local function updateinAPtable()
local tc = #_d.ap_table
dz.log( '>>>>>> _d.ap_table exist with ' .. tc .. ' rows.', dz.LOG_DEBUG )
for i = 1, tc do
if _d.ap_table[i].ap_hex == ap_hex then
-- We got the same airplane.
if ap_distance <= _d.ap_table[i].ap_distance then
-- Update/Overwrite row with information of closest position.
_d.ap_table[i].ap_date = ap_date
_d.ap_table[i].ap_time = ap_time
_d.ap_table[i].ap_hex = ap_hex
_d.ap_table[i].ap_flight = ap_flight
_d.ap_table[i].ap_r = ap_r
_d.ap_table[i].ap_t = ap_t
_d.ap_table[i].ap_desc = ap_desc
_d.ap_table[i].ap_gs = ap_gs
_d.ap_table[i].ap_track = ap_track
_d.ap_table[i].ap_direction = ap_direction
_d.ap_table[i].ap_lat = ap_lat
_d.ap_table[i].ap_long = ap_long
_d.ap_table[i].ap_distance = ap_distance
_d.ap_table[i].ap_geom_rate = ap_geom_rate
_d.ap_table[i].ap_geom_rateString = ap_geom_rateString
_d.ap_table[i].ap_altitude = ap_altitude
_d.ap_table[i].ap_scriptstatus = 'updated'
--_d.ap_table[i].ap_scriptcount = _d.ap_table[i].ap_scriptcount + 1
dz.log( 'Updated entire row for flight ' .. ap_flight .. ' with closer distance ' .. ap_distance .. ' km.', dz.LOG_DEBUG )
else
-- Moves away from current location.
_d.ap_table[i].ap_scriptstatus = 'leaving'
--_d.ap_table[i].ap_scriptcount = _d.ap_table[i].ap_scriptcount + 1
dz.log( 'Updated row for flight ' .. ap_flight .. ' with greater distance ' .. ap_distance .. ' km as leaving.', dz.LOG_DEBUG )
end
end
end
end
local function processResult_table()
--Get values from resulttable to variables.
-- Get date and time.
ap_date = dz.time.dateToDate( dz.time.rawDate,'yyyy-mm-dd', 'dd-mm-yyyy' )
ap_time = dz.time.rawTime
dz.log( '>>>>>> ap_date: ' .. ap_date .. ' and ap_time: ' .. ap_time, dz.LOG_DEBUG )
--Now get remaining stuff from result_table.
local tc = #result_table
for i = 1, tc do
ap_hex = result_table[i].hex -- Required and always supposed to be present.
ap_flight = result_table[i].flight or ap_unknown -- If field is nil, or will evaluate to its second operand.
ap_r = result_table[i].r or ap_unknown -- When nil set to 'unknown'.
ap_t = result_table[i].t or ap_unknown -- When nil set to 'unknown'.
ap_desc = result_table[i].desc or ap_unknown -- When nil set to 'unknown'.
if result_table[i].gs == nil then
ap_gs = ap_unknown
else
ap_gs = _u.round( result_table[i].gs * 1.851999278976, 0 ) -- Calculate groundspeed knots to km.
end
ap_track = result_table[i].track or ap_unknown -- Direction in degree.
if ap_track ~= ap_unknown then
ap_direction = _h.getDirectionfromDegree( ap_track )
-- Translate abbreviation into Dutch abbreviation. E(ast) becomes O(ost) S(outh) becomes Z(uid).
ap_direction = string.gsub( ap_direction, "E", "O", 2 )
ap_direction = string.gsub( ap_direction, "S", "Z", 2 )
else
ap_direction = ap_unknown
end
ap_lat = result_table[i].lat -- Required and always supposed to be present.
ap_long = result_table[i].lon -- Required and always supposed to be present.
dz.log( 'Flight ' .. ap_hex .. ' has Lat en Long: ' .. ap_lat .. ' - ' .. ap_long, dz.LOG_DEBUG )
if ap_lat ~= ap_unknown and ap_long ~= ap_unknown then
ap_distance = _u.round( _h.calculateDistance( lat, long, ap_lat, ap_long ), 1 )
else
ap_distance = ap_unknown
end
ap_geom_rateString = ''
ap_geom_rate = result_table[i].geom_rate
if ap_geom_rate ~= nil and ap_geom_rate > 0 then
-- Ascending.
ap_geom_rateString = ' Stijgt ' .. _u.round ( ap_geom_rate * 0.3048 , 0 ) .. ' m/min.'
elseif ap_geom_rate ~= nil and ap_geom_rate < 0 then
-- Descending.
ap_geom_rateString = ' Daalt ' .. _u.round ( math.abs( ap_geom_rate ) * 0.3048 , 0 ) .. ' m/min.'
end
ap_altitude = 0
--The fieldalt_geom is regularly empty. Therefore we check for alternative.
if tonumber( result_table[i].alt_geom ) ~= nil then
dz.log( 'For flight ' .. ap_hex .. ' alt_geom ' .. result_table[i].alt_geom .. ' is used.', dz.LOG_DEBUG )
-- Preferred fot the height.
ap_altitude = _u.round ( result_table[i].alt_geom * 0.3048 , 0 ) --Calculate height feet to meters.
elseif tonumber( result_table[i].alt_baro ) ~= nil then
dz.log( 'For flight ' .. ap_hex .. ' alt_baro ' .. result_table[i].alt_baro .. ' is used.', dz.LOG_DEBUG )
-- If alt_geom is not available take the alt_baro.
ap_altitude = _u.round ( result_table[i].alt_baro * 0.3048 , 0 ) --Calculate height feet to meters.
end
if ap_altitude <= ap_maxAltitudeM then
if ap_altitude == 0 then
dz.log( 'For flight ' .. ap_hex .. ' altitude is unknown.', dz.LOG_INFO )
ap_altitude = ap_unknown
end
-- It is within the max. height.
dz.log( 'Flight ' .. ap_hex .. ' has a distance of ' .. ap_distance .. ' km.', dz.LOG_DEBUG )
if airplanesExist( ap_hex ) == true then
-- This plane exist already in our table. check and update when needed.
updateinAPtable()
else
-- This plane is Not in the table. So insert this new airplane.
insertinAPtable()
end
else
dz.log( 'Skip flight ' .. ap_hex .. ' that is passing at ' .. ap_altitude .. 'm. is higher then the set ' .. ap_maxAltitudeM .. ' m.', dz.LOG_DEBUG )
end
end
end
local function WritefromAPtable()
if next( _d.ap_table ) ~= nil then
dz.log( 'Check to write from _d.ap_table with ' .. #_d.ap_table .. ' rows.', dz.LOG_DEBUG )
if type( _d.ap_table ) == "table" then
-- For each table row
local tc = #_d.ap_table
for i = 1, tc do
if _d.ap_table[i].ap_scriptstatus == 'leaving' or _d.ap_table[i].ap_scriptcount > ap_forcewriteCounter then
-- This row can be written.
-- Get the previous stored data from device to compare.
local ap_lastdeviceText = dz.devices(ap_text_idx).text -- Holds string of the previous round.
-- Prepare the data we have in this row.
local airplaneText = '<a href="https://globe.airplanes.live/?icao=' .. _d.ap_table[i].ap_hex .. '" target="_blank"><span style="color: Blue">' .. _d.ap_table[i].ap_flight .. '</span></a>' .. ' - ' .. _d.ap_table[i].ap_r .. ' - ' .. _d.ap_table[i].ap_desc ..
'<br>Hoogte: ' .. _d.ap_table[i].ap_altitude .. ' m. Snelheid: ' .. _d.ap_table[i].ap_gs .. ' km/u. Afstand: ' .. _d.ap_table[i].ap_distance .. ' km.' ..
'<br>Richting: ' .. _d.ap_table[i].ap_direction .. _d.ap_table[i].ap_geom_rateString .. ' Tijd: ' .. _d.ap_table[i].ap_time
dz.log( 'Airplane = ' .. airplaneText, dz.LOG_DEBUG )
if airplaneText ~= ap_lastdeviceText then
dz.log( 'Saving flight ' .. _d.ap_table[i].ap_hex .. ' with distance ' .. _d.ap_table[i].ap_distance .. ' km.', dz.LOG_DEBUG )
-- Data is changed, so update the device.
dz.devices(ap_text_idx).updateText( airplaneText )
if ap_counter_idx ~= 99999 then
--Counter is configured, so update the incremental counter device
dz.devices(ap_counter_idx).updateCounter(1)
end
if ap_useCSVfile == 1 then
dz.log( 'Saving flight ' .. _d.ap_table[i].ap_hex .. ' also to csvFile ' .. ap_csvFile, dz.LOG_DEBUG )
local logString = _d.ap_table[i].ap_date .. '\t' .. _d.ap_table[i].ap_time .. '\t' .. _d.ap_table[i].ap_hex .. '\t' .. _d.ap_table[i].ap_flight .. '\t' .. _d.ap_table[i].ap_r .. '\t' .. _d.ap_table[i].ap_t .. '\t' .. _d.ap_table[i].ap_desc .. '\t' .. _d.ap_table[i].ap_gs .. '\t' .. _d.ap_table[i].ap_track .. '\t' .. _d.ap_table[i].ap_direction .. '\t' .. _d.ap_table[i].ap_lat .. '\t' .. _d.ap_table[i].ap_long .. '\t' .. _d.ap_table[i].ap_distance .. '\t' .. _d.ap_table[i].ap_geom_rateString .. '\t' .. _d.ap_table[i].ap_altitude
if _u.fileExists( ap_csvFile ) then
-- File exist so append content.
_h.AppendStringToFile( dz, ap_csvFile, logString .. '\n' )
else
-- File does not exist. Start with header information.
local headerString = 'ap_date\tap_time\tap_hex\tap_flight\tap_r\tap_t\tap_desc\tap_gs\tap_track\tap_direction\tap_lat\tap_long\tap_distance\tap_geom_rateString\tap_altitude'
_h.AppendStringToFile( dz, ap_csvFile, headerString .. '\n' )
_h.AppendStringToFile( dz, ap_csvFile, logString .. '\n' )
end
end
end
-- When it is stored, we can mark the row to be removed.
_d.ap_table[i].ap_scriptstatus = 'tobedeleted'
-- Mark the rownumber to be deleted.
dz.log( 'Marked the row for flight ' .. _d.ap_table[i].ap_hex .. ' to be deleted.', dz.LOG_DEBUG )
else
_d.ap_table[i].ap_scriptcount = _d.ap_table[i].ap_scriptcount + 1
dz.log( 'Counter for forced writing flight ' .. _d.ap_table[i].ap_hex .. ' = ' .. _d.ap_table[i].ap_scriptcount .. '.', dz.LOG_DEBUG )
end
end
end
else
dz.log( 'Nothing left in _d.ap_table. Nothing left to do.', dz.LOG_DEBUG )
end
end
local function RemovefromAPtable()
-- We can remove during iteration by iterating from back to front.
for i=#_d.ap_table,1,-1 do
if _d.ap_table[i].ap_scriptstatus == 'tobedeleted' then
--Row is marked for deletion.
local flight = _d.ap_table[i].ap_hex
table.remove( _d.ap_table, i )
dz.log( 'Removed row for flight ' .. flight .. ' from _d.ap_table.', dz.LOG_DEBUG )
end
end
end
-- Now start to do something ============
if (triggeredItem.isTimer) then
-- Retrieve the data every ap_pollFreq second per 5 minutes.
for seconds = ap_pollFreq, 300, ap_pollFreq do
dz.openURL({
url = 'https://api.airplanes.live/v2/point/' .. lat .. '/' .. long ..'/' .. ap_maxRadiusMiles,
method = 'GET',
callback = 'airplanes'
}).afterSec(seconds)
end
end
if (triggeredItem.isHTTPResponse) then
-- Process the obtained data.
-- Check the response and process the data.
if (triggeredItem.ok and triggeredItem.isJSON) then
dz.log( 'Item and JSON - OK', dz.LOG_DEBUG )
-- We have some result.
result_table = triggeredItem.json.ac
if type(result_table) == "table" then
dz.log( 'result_table: type = ' .. type(result_table), dz.LOG_DEBUG )
if triggeredItem.json.total > 0 then
-- We see at least one plane. Retreive thje data from result_table.
processResult_table()
else
dz.log( 'Total flights found in response = 0.', dz.LOG_DEBUG )
end
-- See if we have something (left) in the table to write to device.
WritefromAPtable()
RemovefromAPtable()
else
dz.log( 'No result_table found', dz.LOG_ERROR )
end
else
dz.log( 'Item or JSON - NOT OK', dz.LOG_DEBUG )
end
end
end
}
-----------------------------------------------------------------------
-- Example of the data we get in with description of the fields -------
--{
-- "ac": [
-- {
-- "hex": "485064", -the 24-bit ICAO identifier of the aircraft, as 6 hex digits. The identifier may start with ‘~’, this means that the address is a non-ICAO address
-- "type": "adsb_icao", -type of underlying messages / best source of current data for this position / aircraft: ( order of which data is preferentially used )
-- "flight": "KLM66C", -callsign, the flight name or aircraft registration as 8 chars"
-- "r": "PH-EZY", - aircraft registration pulled from database
-- "t": "E190", -aircraft type pulled from database
-- "desc": "EMBRAER ERJ-190-100",
-- "ownOp": "Klm Cityhopper",
-- "alt_baro": 1875, -the aircraft barometric altitude in feet as a number OR “ground” as a string
-- "alt_geom": 2200, -geometric (GNSS / INS) altitude in feet referenced to the WGS84 ellipsoid
-- "gs": 254.5, -ground speed in knots
-- "ias": 248, -indicated air speed in knots
-- "tas": 256, -true air speed in knots
-- "mach": 0.388, -Mach number
-- "wd": 19, -wind direction (and wind speed) are calculated from ground track, true heading, true airspeed and ground speed
-- "ws": 6, -Windspeed
-- "oat": 6, -outer/static air temperature (C) *(and total air temperature (C)) are calculated from mach number and true airspeed (typically somewhat inaccurate at lower altitudes / mach numbers below 0.5, calculation is inhibited for mach < 0.395)
-- "tat": 15, -total air temperature (C)
-- "track": 93.6, -true track over ground in degrees (0-359)
-- "track_rate": 0.09, -Rate of change of track, degrees/second
-- "roll": -0.18, -Roll, degrees, negative is left roll
-- "mag_heading": 90, -Heading, degrees clockwise from magnetic north
-- "true_heading": 92.4, -Heading, degrees clockwise from true north (usually only transmitted on ground, in the air usually derived from the magnetic heading using magnetic model WMM2020)
-- "baro_rate": -192, -Rate of change of barometric altitude, feet/minute
-- "geom_rate": -128, -Rate of change of geometric (GNSS / INS) altitude, feet/minute
-- "squawk": "7631", -Mode A code (Squawk), encoded as 4 octal digits
-- "emergency": "none", -ADS-B emergency/priority status, a superset of the 7×00 squawks (2.2.3.2.7.8.1.1) (none, general, lifeguard, minfuel, nordo, unlawful, downed, reserved)
-- "category": "A3", -emitter category to identify particular aircraft or vehicle classes (values A0 – D7)
-- "nav_qnh": 1018.4, -altimeter setting (QFE or QNH/QNE), hPa
-- "nav_altitude_mcp": 2016, -selected altitude from the Mode Control Panel / Flight Control Unit (MCP/FCU) or equivalent equipment
-- "nav_altitude_fms": 2000, -selected altitude from the Flight Manaagement System (FMS) (2.2.3.2.7.1.3.3)
-- "nav_modes": [ -set of engaged automation modes: ‘autopilot’, ‘vnav’, ‘althold’, ‘approach’, ‘lnav’, ‘tcas’
-- "autopilot",
-- "tcas"
-- ],
-- "lat": 52.166748, -the aircraft position in decimal degrees
-- "lon": 4.638977, -the aircraft position in decimal degrees
-- "nic": 8, -Navigation Integrity Category (2.2.3.2.7.2.6)
-- "rc": 186, -Radius of Containment, meters; a measure of position integrity derived from NIC & supplementary bits. (2.2.3.2.7.2.6, Table 2-69)
-- "seen_pos": 0.179, -how long ago (in seconds before “now”) the position was last updated
-- "recentReceiverIds": [
-- "8a6d5aed-e6d2-4a79",
-- "462a41f7-9808-5db2",
-- "b29d8c19-8849-41bb",
-- "8a4d3496-8f73-11ea",
-- "c21ace72-6613-4a53",
-- "4284e70f-e716-e5ab",
-- "01122182-d7a1-11ec",
-- "40d9183e-8c3f-47a1",
-- "6293f6c0-1ecf-4ad2",
-- "e74ec3c3-c566-e5e2",
-- "cdf69800-c853-47b0",
-- "f786031b-56b9-4b97",
-- "7715abf6-175d-4d44"
-- ],
-- "version": 2, -ADS-B Version Number 0, 1, 2 (3-7 are reserved) (2.2.3.2.7.5)
-- "nic_baro": 1, -Navigation Integrity Category for Barometric Altitude (2.2.5.1.35)
-- "nac_p": 10, -Navigation Accuracy for Position (2.2.5.1.35)
-- "nac_v": 2, -Navigation Accuracy for Velocity (2.2.5.1.19)
-- "sil": 3, -Source Integity Level (2.2.5.1.40)
-- "sil_type": "perhour", -interpretation of SIL: unknown, perhour, persample
-- "gva": 2, -Geometric Vertical Accuracy (2.2.3.2.7.2.8)
-- "sda": 2, -System Design Assurance (2.2.3.2.7.2.4.6)
-- "alert": 0, -Flight status alert bit (2.2.3.2.3.2)
-- "spi": 0, -Flight status special position identification bit (2.2.3.2.3.2)
-- "mlat": [], -list of fields derived from MLAT data
-- "tisb": [], -list of fields derived from TIS-B data
-- "messages": 119867, -total number of Mode S messages received from this aircraft
-- "seen": 0.2, -how long ago (in seconds before “now”) a message was last received from this aircraft
-- "rssi": -6.5, -recent average RSSI (signal power), in dbFS; this will always be negative.
-- "dst": 1.883,
-- "dir": 318.9
-- }
-- ],
-- "msg": "No error", -Shows if there is an error, default is “No error”.
-- "now": 1716574591594, -The time this file was generated, in seconds since Jan 1 1970 00:00:00 GMT (the Unix epoch)
-- "total": 1, -Total aircraft returned.
-- "ctime": 1716574591594, -The time this file was cached, in seconds since Jan 1 1970 00:00:00 GMT (the Unix epoch).
-- "ptime": 0 uThe server processing time this request required in milliseconds.
--}
-- That's All --------------------------------------------------