|
1 | | -# SPDX-FileCopyrightText: 2023 DJDevon3 |
| 1 | +# SPDX-FileCopyrightText: 2024 DJDevon3 |
2 | 2 | # SPDX-License-Identifier: MIT |
3 | | -# Coded for Circuit Python 8.1 |
4 | | -# DJDevon3 ESP32-S3 OpenSkyNetwork_Private_API_Example |
| 3 | +# Coded for Circuit Python 8.2.x |
| 4 | +"""OpenSky-Network.org Private API Example""" |
| 5 | +# pylint: disable=import-error |
5 | 6 |
|
6 | | -import json |
7 | 7 | import os |
8 | | -import ssl |
9 | 8 | import time |
10 | 9 |
|
11 | | -import circuitpython_base64 as base64 |
12 | | -import socketpool |
| 10 | +import adafruit_connection_manager |
13 | 11 | import wifi |
| 12 | +from adafruit_binascii import b2a_base64 |
14 | 13 |
|
15 | 14 | import adafruit_requests |
16 | 15 |
|
|
19 | 18 | # All active flights JSON: https://opensky-network.org/api/states/all # PICK ONE! :) |
20 | 19 | # JSON order: transponder, callsign, country |
21 | 20 | # ACTIVE transpondes only, for multiple "c822af&icao24=cb3993&icao24=c63923" |
22 | | -transponder = "7c6b2d" |
| 21 | +TRANSPONDER = "471efd" |
23 | 22 |
|
24 | | -# Initialize WiFi Pool (There can be only 1 pool & top of script) |
25 | | -pool = socketpool.SocketPool(wifi.radio) |
| 23 | +# Github developer token required. |
| 24 | +username = os.getenv("GITHUB_USERNAME") |
| 25 | +token = os.getenv("GITHUB_TOKEN") |
26 | 26 |
|
27 | | -# Time between API refreshes |
| 27 | +# Get WiFi details, ensure these are setup in settings.toml |
| 28 | +ssid = os.getenv("CIRCUITPY_WIFI_SSID") |
| 29 | +password = os.getenv("CIRCUITPY_WIFI_PASSWORD") |
| 30 | +osnusername = os.getenv("OSN_USERNAME") # Website Credentials |
| 31 | +osnpassword = os.getenv("OSN_PASSWORD") # Website Credentials |
| 32 | + |
| 33 | +# API Polling Rate |
28 | 34 | # 900 = 15 mins, 1800 = 30 mins, 3600 = 1 hour |
29 | 35 | # OpenSky-Networks IP bans for too many requests, check rate limit. |
30 | 36 | # https://openskynetwork.github.io/opensky-api/rest.html#limitations |
31 | | -sleep_time = 1800 |
| 37 | +SLEEP_TIME = 1800 |
32 | 38 |
|
33 | | -# Get WiFi details, ensure these are setup in settings.toml |
34 | | -ssid = os.getenv("CIRCUITPY_WIFI_SSID") |
35 | | -password = os.getenv("CIRCUITPY_WIFI_PASSWORD") |
36 | | -osnu = os.getenv("OSN_Username") |
37 | | -osnp = os.getenv("OSN_Password") |
38 | | - |
39 | | -osn_cred = str(osnu) + ":" + str(osnp) |
40 | | -bytes_to_encode = b" " + str(osn_cred) + " " |
41 | | -base64_string = base64.encodebytes(bytes_to_encode) |
42 | | -base64cred = repr(base64_string)[2:-1] |
43 | | - |
44 | | -Debug_Auth = False # STREAMER WARNING this will show your credentials! |
45 | | -if Debug_Auth: |
46 | | - osn_cred = str(osnu) + ":" + str(osnp) |
47 | | - bytes_to_encode = b" " + str(osn_cred) + " " |
48 | | - print(repr(bytes_to_encode)) |
49 | | - base64_string = base64.encodebytes(bytes_to_encode) |
50 | | - print(repr(base64_string)[2:-1]) |
51 | | - base64cred = repr(base64_string)[2:-1] |
52 | | - print("Decoded Bytes:", str(base64cred)) |
| 39 | +# Set debug to True for full JSON response. |
| 40 | +# WARNING: makes credentials visible |
| 41 | +DEBUG = False |
| 42 | + |
| 43 | +# Initalize Wifi, Socket Pool, Request Session |
| 44 | +pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio) |
| 45 | +ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio) |
| 46 | +requests = adafruit_requests.Session(pool, ssl_context) |
| 47 | + |
| 48 | +# -- Base64 Conversion -- |
| 49 | +OSN_CREDENTIALS = str(osnusername) + ":" + str(osnpassword) |
| 50 | +OSN_CREDENTIALS_B = b"" + str(OSN_CREDENTIALS) + "" |
| 51 | +BASE64_ASCII = b2a_base64(OSN_CREDENTIALS_B) |
| 52 | +BASE64_STRING = str(BASE64_ASCII) # bytearray |
| 53 | +TRUNCATED_BASE64_STRING = BASE64_STRING[2:-1] # truncate bytearray head/tail |
| 54 | + |
| 55 | +if DEBUG: |
| 56 | + print("Original Binary Data: ", OSN_CREDENTIALS_B) |
| 57 | + print("Base64 ByteArray: ", BASE64_ASCII) |
| 58 | + print(f"Base64 String: {TRUNCATED_BASE64_STRING}") |
53 | 59 |
|
54 | 60 | # Requests URL - icao24 is their endpoint required for a transponder |
55 | 61 | # example https://opensky-network.org/api/states/all?icao24=a808c5 |
56 | | -# OSN private requires your username:password to be base64 encoded |
57 | | -osn_header = {"Authorization": "Basic " + str(base64cred)} |
58 | | -OPENSKY_SOURCE = "https://opensky-network.org/api/states/all?" + "icao24=" + transponder |
| 62 | +# OSN private: requires your website username:password to be base64 encoded |
| 63 | +OSN_HEADER = {"Authorization": "Basic " + str(TRUNCATED_BASE64_STRING)} |
| 64 | +OPENSKY_SOURCE = "https://opensky-network.org/api/states/all?" + "icao24=" + TRANSPONDER |
59 | 65 |
|
60 | 66 |
|
61 | | -# Converts seconds to human readable minutes/hours/days |
62 | | -def time_calc(input_time): # input_time in seconds |
| 67 | +def time_calc(input_time): |
| 68 | + """Converts seconds to minutes/hours/days""" |
63 | 69 | if input_time < 60: |
64 | | - sleep_int = input_time |
65 | | - time_output = f"{sleep_int:.0f} seconds" |
66 | | - elif 60 <= input_time < 3600: |
67 | | - sleep_int = input_time / 60 |
68 | | - time_output = f"{sleep_int:.0f} minutes" |
69 | | - elif 3600 <= input_time < 86400: |
70 | | - sleep_int = input_time / 60 / 60 |
71 | | - time_output = f"{sleep_int:.1f} hours" |
72 | | - else: |
73 | | - sleep_int = input_time / 60 / 60 / 24 |
74 | | - time_output = f"{sleep_int:.1f} days" |
75 | | - return time_output |
| 70 | + return f"{input_time:.0f} seconds" |
| 71 | + if input_time < 3600: |
| 72 | + return f"{input_time / 60:.0f} minutes" |
| 73 | + if input_time < 86400: |
| 74 | + return f"{input_time / 60 / 60:.0f} hours" |
| 75 | + return f"{input_time / 60 / 60 / 24:.1f} days" |
76 | 76 |
|
77 | 77 |
|
78 | 78 | def _format_datetime(datetime): |
79 | | - return "{:02}/{:02}/{} {:02}:{:02}:{:02}".format( |
80 | | - datetime.tm_mon, |
81 | | - datetime.tm_mday, |
82 | | - datetime.tm_year, |
83 | | - datetime.tm_hour, |
84 | | - datetime.tm_min, |
85 | | - datetime.tm_sec, |
| 79 | + return ( |
| 80 | + f"{datetime.tm_mon:02}/" |
| 81 | + + f"{datetime.tm_mday:02}/" |
| 82 | + + f"{datetime.tm_year:02} " |
| 83 | + + f"{datetime.tm_hour:02}:" |
| 84 | + + f"{datetime.tm_min:02}:" |
| 85 | + + f"{datetime.tm_sec:02}" |
86 | 86 | ) |
87 | 87 |
|
88 | 88 |
|
89 | | -# Connect to Wi-Fi |
90 | | -print("\n===============================") |
91 | | -print("Connecting to WiFi...") |
92 | | -request = adafruit_requests.Session(pool, ssl.create_default_context()) |
93 | | -while not wifi.radio.ipv4_address: |
| 89 | +while True: |
| 90 | + # Connect to Wi-Fi |
| 91 | + print("\nConnecting to WiFi...") |
| 92 | + while not wifi.radio.ipv4_address: |
| 93 | + try: |
| 94 | + wifi.radio.connect(ssid, password) |
| 95 | + except ConnectionError as e: |
| 96 | + print("❌ Connection Error:", e) |
| 97 | + print("Retrying in 10 seconds") |
| 98 | + print("✅ Wifi!") |
| 99 | + |
94 | 100 | try: |
95 | | - wifi.radio.connect(ssid, password) |
96 | | - except ConnectionError as e: |
97 | | - print("Connection Error:", e) |
98 | | - print("Retrying in 10 seconds") |
99 | | - time.sleep(10) |
100 | | -print("Connected!\n") |
| 101 | + print(" | Attempting to GET OpenSky-Network Single Flight JSON!") |
| 102 | + try: |
| 103 | + opensky_response = requests.get(url=OPENSKY_SOURCE, headers=OSN_HEADER) |
| 104 | + opensky_json = opensky_response.json() |
| 105 | + except ConnectionError as e: |
| 106 | + print("Connection Error:", e) |
| 107 | + print("Retrying in 10 seconds") |
101 | 108 |
|
102 | | -while True: |
103 | | - # STREAMER WARNING this will show your credentials! |
104 | | - debug_request = False # Set True to see full request |
105 | | - if debug_request: |
106 | | - print("Full API HEADER: ", str(osn_header)) |
107 | | - print("Full API GET URL: ", OPENSKY_SOURCE) |
108 | | - print("===============================") |
| 109 | + print(" | ✅ OpenSky-Network JSON!") |
109 | 110 |
|
110 | | - print("\nAttempting to GET OpenSky-Network Data!") |
111 | | - opensky_response = request.get(url=OPENSKY_SOURCE, headers=osn_header).json() |
| 111 | + if DEBUG: |
| 112 | + print("Full API GET URL: ", OPENSKY_SOURCE) |
| 113 | + print(opensky_json) |
112 | 114 |
|
113 | | - # Print Full JSON to Serial (doesn't show credentials) |
114 | | - debug_response = False # Set True to see full response |
115 | | - if debug_response: |
116 | | - dump_object = json.dumps(opensky_response) |
117 | | - print("JSON Dump: ", dump_object) |
| 115 | + # ERROR MESSAGE RESPONSES |
| 116 | + if "timestamp" in opensky_json: |
| 117 | + osn_timestamp = opensky_json["timestamp"] |
| 118 | + print(f"❌ Timestamp: {osn_timestamp}") |
118 | 119 |
|
119 | | - # Key:Value Serial Debug (doesn't show credentials) |
120 | | - osn_debug_keys = True # Set True to print Serial data |
121 | | - if osn_debug_keys: |
122 | | - try: |
123 | | - osn_flight = opensky_response["time"] |
124 | | - print("Current Unix Time: ", osn_flight) |
125 | | - |
126 | | - current_struct_time = time.localtime(osn_flight) |
127 | | - current_date = "{}".format(_format_datetime(current_struct_time)) |
128 | | - print(f"Unix to Readable Time: {current_date}") |
129 | | - |
130 | | - # Current flight data for single callsign (right now) |
131 | | - osn_single_flight_data = opensky_response["states"] |
132 | | - |
133 | | - if osn_single_flight_data is not None: |
134 | | - print("Flight Data: ", osn_single_flight_data) |
135 | | - transponder = opensky_response["states"][0][0] |
136 | | - print("Transponder: ", transponder) |
137 | | - callsign = opensky_response["states"][0][1] |
138 | | - print("Callsign: ", callsign) |
139 | | - country = opensky_response["states"][0][2] |
140 | | - print("Flight Country: ", country) |
| 120 | + if "message" in opensky_json: |
| 121 | + osn_message = opensky_json["message"] |
| 122 | + print(f"❌ Message: {osn_message}") |
| 123 | + |
| 124 | + if "error" in opensky_json: |
| 125 | + osn_error = opensky_json["error"] |
| 126 | + print(f"❌ Error: {osn_error}") |
| 127 | + |
| 128 | + if "path" in opensky_json: |
| 129 | + osn_path = opensky_json["path"] |
| 130 | + print(f"❌ Path: {osn_path}") |
| 131 | + |
| 132 | + if "status" in opensky_json: |
| 133 | + osn_status = opensky_json["status"] |
| 134 | + print(f"❌ Status: {osn_status}") |
| 135 | + |
| 136 | + # Current flight data for single callsign (right now) |
| 137 | + osn_single_flight_data = opensky_json["states"] |
| 138 | + |
| 139 | + if osn_single_flight_data is not None: |
| 140 | + if DEBUG: |
| 141 | + print(f" | | Single Flight Data: {osn_single_flight_data}") |
| 142 | + |
| 143 | + last_contact = opensky_json["states"][0][4] |
| 144 | + # print(f" | | Last Contact Unix Time: {last_contact}") |
| 145 | + lc_struct_time = time.localtime(last_contact) |
| 146 | + lc_readable_time = f"{_format_datetime(lc_struct_time)}" |
| 147 | + print(f" | | Last Contact: {lc_readable_time}") |
| 148 | + |
| 149 | + flight_transponder = opensky_json["states"][0][0] |
| 150 | + print(f" | | Transponder: {flight_transponder}") |
| 151 | + |
| 152 | + callsign = opensky_json["states"][0][1] |
| 153 | + print(f" | | Callsign: {callsign}") |
| 154 | + |
| 155 | + squawk = opensky_json["states"][0][14] |
| 156 | + print(f" | | Squawk: {squawk}") |
| 157 | + |
| 158 | + country = opensky_json["states"][0][2] |
| 159 | + print(f" | | Origin: {country}") |
| 160 | + |
| 161 | + longitude = opensky_json["states"][0][5] |
| 162 | + print(f" | | Longitude: {longitude}") |
| 163 | + |
| 164 | + latitude = opensky_json["states"][0][6] |
| 165 | + print(f" | | Latitude: {latitude}") |
| 166 | + |
| 167 | + # Return Air Flight data if not on ground |
| 168 | + on_ground = opensky_json["states"][0][8] |
| 169 | + if on_ground is True: |
| 170 | + print(f" | | On Ground: {on_ground}") |
141 | 171 | else: |
142 | | - print("Flight has no active data or you're polling too fast.") |
143 | | - |
144 | | - print("\nFinished!") |
145 | | - print("Board Uptime: ", time_calc(time.monotonic())) |
146 | | - print("Next Update: ", time_calc(sleep_time)) |
147 | | - time.sleep(sleep_time) |
148 | | - print("===============================") |
149 | | - |
150 | | - except (ConnectionError, ValueError, NameError) as e: |
151 | | - print("OSN Connection Error:", e) |
152 | | - print("Next Retry: ", time_calc(sleep_time)) |
153 | | - time.sleep(sleep_time) |
| 172 | + altitude = opensky_json["states"][0][7] |
| 173 | + print(f" | | Barometric Altitude: {altitude}") |
| 174 | + |
| 175 | + velocity = opensky_json["states"][0][9] |
| 176 | + if velocity != "null": |
| 177 | + print(f" | | Velocity: {velocity}") |
| 178 | + |
| 179 | + vertical_rate = opensky_json["states"][0][11] |
| 180 | + if vertical_rate != "null": |
| 181 | + print(f" | | Vertical Rate: {vertical_rate}") |
| 182 | + |
| 183 | + else: |
| 184 | + print(" | | ❌ Flight has no active data or you're polling too fast.") |
| 185 | + |
| 186 | + opensky_response.close() |
| 187 | + print("✂️ Disconnected from OpenSky-Network API") |
| 188 | + |
| 189 | + print("\nFinished!") |
| 190 | + print(f"Board Uptime: {time_calc(time.monotonic())}") |
| 191 | + print(f"Next Update: {time_calc(SLEEP_TIME)}") |
| 192 | + print("===============================") |
| 193 | + |
| 194 | + except (ValueError, RuntimeError) as e: |
| 195 | + print(f"Failed to get data, retrying\n {e}") |
| 196 | + time.sleep(60) |
| 197 | + break |
| 198 | + time.sleep(SLEEP_TIME) |
0 commit comments