|
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