Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion PseudoCode.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
### How It works

- Takes data about every (x seconds configurable) from OpenSky Network or ADS-B Exchange and compares it to previous data with what I've defined as a landing or takeoff event.
- A takeoff event is the plane is not on the ground, below 10k feet and ((previously no data and now getting data) or was previously on the ground).
- A landing event is previously below 10k feet and (previously getting data, no longer getting data and previously not on the ground) or (now on the ground and previously not on the ground).
- Given the coordinates of the aircraft the nearest airport is found in an airport database from <https://ourairports.com/data> the distance is calculated using the Haversine formula. The state, region and country are also found in this database with the airport.
- At the time of takeoff a takeoff time is set, which is referenced in the landing event to calculate approximate total flight time.
- A Static map image is created based off location name. (Google Static Maps API) or a screenshot of <https://global.adsbexchange.com/> is created using Selenium/ChromeDriver The selected plane is locked on in the screenshot.
- If the landing event or takeoff event is true, It will output to any of the following built-in output methods. (Twitter, Pushbullet, and Discord all of which can be setup and enabled in each planes config file. Outputs the location name, map image and flight time on landing. (Tweepy and "Pushbullet.py" and Discord_webhooks)
- If the landing event or takeoff event is true, It will output to any of the following built-in output methods. (Twitter, Pushbullet, and Discord all of which can be setup and enabled in each planes config file. Outputs the location name, map image and flight time on landing. (Tweepy and "Pushbullet.py" and Discord_webhooks)
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Notify if configured planes have taken off or landed using Python with OpenSky(f

![Discord Output Example](./ExImages/DiscordEX.png?raw=true)

#### More examples in the ExImages folder
#### More examples in the ExImages folder

[ExImages](./ExImages)

Expand All @@ -19,7 +19,7 @@ I made this program so I could track Elon Musk's Jet and share with others of hi

### Contributing

Im open to any help or suggestions, I realize theirs much better ways im sure to do alot of my methods, im only a noob. I'll accept pull requests. If you'd like to discuss join <https://JacksTech.net/Discord>
Im open to any help or suggestions, I realize theirs much better ways im sure to do alot of my methods, im only a noob. I'll accept pull requests. If you'd like to discuss join <https://JacksTech.net/Discord>

### [Algorithm](PseudoCode.md)

Expand Down Expand Up @@ -49,6 +49,7 @@ Selenium/ChromeDriver is used to take a screenshot of the plane on globe.adsbexc
```bash
sudo apt-get install chromium
```

These output methods once installed can be configured in planes config you create, using the example plane1.ini

### Install Screen to run in the background
Expand All @@ -69,12 +70,12 @@ cd plane-notify

- edit them with nano or vi on the running machine or on your pc and transfer the config to where you will be running the bot
- Pick between OpenSky and ADS-B Exchange
- The OpenSky API is free for everyone but the data is not as good as ADS-B Exchange. The ADS-B Exchange API is not free and this program will not work for the Rapid API from ADS-B Exchange. It only works with the API that they give when you have a partnership with ADS-B Exchange. It is not cheap to get the ADS-B Exchange full API, Don't contact them unless your ready to pay.
- If you'd like to add support for ADS-B Exchanges RapidAPI feel free to work on it, and submit a merge request.
- The OpenSky API is free for everyone but the data is not as good as ADS-B Exchange. The ADS-B Exchange API is not free and this program will not work for the Rapid API from ADS-B Exchange. It only works with the API that they give when you have a partnership with ADS-B Exchange. It is not cheap to get the ADS-B Exchange full API, Don't contact them unless your ready to pay.
- If you'd like to add support for ADS-B Exchanges RapidAPI feel free to work on it, and submit a merge request.
- If you've setup multiple planes and want to use ADSB Exchange as your source you must have /all endpoint access to their API or it won't work.
- Pick the correct api version for ADS-B Exchange.
- Proxy is if your running multiple programs that use the ADSB Exchange, setup the proxy from lemonodor so you don't abuse the ADSB Exchange API, otherwise leave enable false.
- When using OpenSky theres more bugs because I mainly use ADS-B Exchange and work less on the OpenSky Implementation.
- When using OpenSky theres more bugs because I mainly use ADS-B Exchange and work less on the OpenSky Implementation.

### Configure individual planes

Expand All @@ -101,7 +102,7 @@ Install [docker from their website](https://docs.docker.com/get-docker/). Run th
docker-compose up -d
```

After running this command, dut to the `-d` flag the container will be running in the background. To see the logs of the docker
After running this command, dut to the `-d` flag the container will be running in the background. To see the logs of the docker

### TODO

Expand Down
193 changes: 128 additions & 65 deletions __main__.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,84 @@
import sys
import configparser
import time
from colorama import Fore, Back, Style
import platform
import traceback

if platform.system() == "Windows":
from colorama import init

init(convert=True)
from planeClass import Plane
from datetime import datetime
import pytz
import os
import signal

abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)
import sys
sys.path.extend([os.getcwd()])
#Dependency Handling
# Dependency Handling
if not os.path.isdir("./dependencies/"):
os.mkdir("./dependencies/")
required_files = [("Roboto-Regular.ttf", 'https://github.com/googlefonts/roboto/blob/main/src/hinted/Roboto-Regular.ttf?raw=true'), ('airports.csv', 'https://ourairports.com/data/airports.csv'), ('regions.csv', 'https://ourairports.com/data/regions.csv'), ('ADSBX_Logo.png', "https://www.adsbexchange.com/wp-content/uploads/cropped-Stealth.png"), ('Mictronics_db.zip', "https://www.mictronics.de/aircraft-database/indexedDB.php")]
required_files = [
(
"Roboto-Regular.ttf",
"https://github.com/googlefonts/roboto/blob/main/src/hinted/Roboto-Regular.ttf?raw=true",
),
("airports.csv", "https://ourairports.com/data/airports.csv"),
("regions.csv", "https://ourairports.com/data/regions.csv"),
(
"ADSBX_Logo.png",
"https://www.adsbexchange.com/wp-content/uploads/cropped-Stealth.png",
),
("Mictronics_db.zip", "https://www.mictronics.de/aircraft-database/indexedDB.php"),
]
for file in required_files:
file_name = file[0]
url = file[1]
if not os.path.isfile("./dependencies/" + file_name):
print(file_name, "does not exist downloading now")
try:
import requests
file_content = requests.get(url)

open(("./dependencies/" + file_name), 'wb').write(file_content.content)
except Exception as e:
raise e("Error getting", file_name, "from", url)
else:
print("Successfully got", file_name)
else:
print("Already have", file_name, "continuing")
if os.path.isfile("./dependencies/" + required_files[4][0]) and not os.path.isfile("./dependencies/aircrafts.json"):
file_name = file[0]
url = file[1]
if not os.path.isfile("./dependencies/" + file_name):
print(file_name, "does not exist downloading now")
try:
import requests

file_content = requests.get(url)

open(("./dependencies/" + file_name), "wb").write(file_content.content)
except Exception as e:
raise e("Error getting", file_name, "from", url)
else:
print("Successfully got", file_name)
else:
print("Already have", file_name, "continuing")
if os.path.isfile("./dependencies/" + required_files[4][0]) and not os.path.isfile(
"./dependencies/aircrafts.json"
):
print("Extracting Mictronics DB")
from zipfile import ZipFile
with ZipFile("./dependencies/" + required_files[4][0], 'r') as mictronics_db:

with ZipFile("./dependencies/" + required_files[4][0], "r") as mictronics_db:
mictronics_db.extractall("./dependencies/")

main_config = configparser.ConfigParser()
print(os.getcwd())
main_config.read('./configs/mainconf.ini')
source = main_config.get('DATA', 'SOURCE')
if main_config.getboolean('DISCORD', 'ENABLE'):
from defDiscord import sendDis
sendDis("Started", main_config)
main_config.read("./configs/mainconf.ini")
source = main_config.get("DATA", "SOURCE")
if main_config.getboolean("DISCORD", "ENABLE"):
from defDiscord import sendDis

sendDis("Started", main_config)


def service_exit(signum, frame):
if main_config.getboolean('DISCORD', 'ENABLE'):
if main_config.getboolean("DISCORD", "ENABLE"):
from defDiscord import sendDis

sendDis("Service Stop", main_config)
raise SystemExit("Service Stop")


signal.signal(signal.SIGTERM, service_exit)
if os.path.isfile("lookup_route.py"):
print("Route lookup is enabled")
Expand All @@ -63,44 +88,58 @@ def service_exit(signum, frame):
try:
print("Source is set to", source)
import sys
#Setup plane objects from plane configs

# Setup plane objects from plane configs
planes = {}
print("Found the following configs")
for dirpath, dirname, filename in os.walk("./configs"):
for filename in [f for f in filename if f.endswith(".ini") and f != "mainconf.ini"]:
if not "disabled" in dirpath:
print(os.path.join(dirpath, filename))
plane_config = configparser.ConfigParser()
plane_config.read((os.path.join(dirpath, filename)))
#Creates a Key labeled the ICAO of the plane, with the value being a plane object
planes[plane_config.get('DATA', 'ICAO').upper()] = Plane(plane_config.get('DATA', 'ICAO'), os.path.join(dirpath, filename), plane_config)
for filename in [
f for f in filename if f.endswith(".ini") and f != "mainconf.ini"
]:
if not "disabled" in dirpath:
print(os.path.join(dirpath, filename))
plane_config = configparser.ConfigParser()
plane_config.read((os.path.join(dirpath, filename)))
# Creates a Key labeled the ICAO of the plane, with the value being a plane object
planes[plane_config.get("DATA", "ICAO").upper()] = Plane(
plane_config.get("DATA", "ICAO"),
os.path.join(dirpath, filename),
plane_config,
)

running_Count = 0
failed_count = 0
try:
tz = pytz.timezone(main_config.get('DATA', 'TZ'))
tz = pytz.timezone(main_config.get("DATA", "TZ"))
except pytz.exceptions.UnknownTimeZoneError:
tz = pytz.UTC
last_ra_count = None
while True:
datetime_tz = datetime.now(tz)
if datetime_tz.hour == 0 and datetime_tz.minute == 0:
running_Count = 0
running_Count +=1
running_Count += 1
start_time = time.time()
header = ("-------- " + str(running_Count) + " -------- " + str(datetime_tz.strftime("%I:%M:%S %p")) + " ---------------------------------------------------------------------------")
print (Back.GREEN + Fore.BLACK + header[0:100] + Style.RESET_ALL)
header = (
"-------- "
+ str(running_Count)
+ " -------- "
+ str(datetime_tz.strftime("%I:%M:%S %p"))
+ " ---------------------------------------------------------------------------"
)
print(Back.GREEN + Fore.BLACK + header[0:100] + Style.RESET_ALL)
if source == "ADSBX":
#ACAS data
# ACAS data
from defADSBX import pull_date_ras
import ast

today = datetime.utcnow()
date = today.strftime("%Y/%m/%d")
ras = pull_date_ras(date)
sorted_ras = {}
if ras is not None:
#Testing RAs
#if last_ra_count is not None:
# Testing RAs
# if last_ra_count is not None:
# with open('./testing/acastest.json') as f:
# data = f.readlines()
# ras += data
Expand All @@ -109,33 +148,34 @@ def service_exit(signum, frame):
print(abs(ra_count - last_ra_count), "new Resolution Advisories")
for ra_num, ra in enumerate(ras[last_ra_count:]):
ra = ast.literal_eval(ra)
if ra['hex'].upper() in planes.keys():
if ra['hex'].upper() not in sorted_ras.keys():
sorted_ras[ra['hex'].upper()] = [ra]
if ra["hex"].upper() in planes.keys():
if ra["hex"].upper() not in sorted_ras.keys():
sorted_ras[ra["hex"].upper()] = [ra]
else:
sorted_ras[ra['hex'].upper()].append(ra)
sorted_ras[ra["hex"].upper()].append(ra)
else:
print("No new Resolution Advisories")
last_ra_count = ra_count
for key, obj in planes.items():
if sorted_ras != {} and key in sorted_ras.keys():
print(key, "has", len(sorted_ras[key]), "RAs")
obj.check_new_ras(sorted_ras[key])
print(key, "has", len(sorted_ras[key]), "RAs")
obj.check_new_ras(sorted_ras[key])
obj.expire_ra_types()
#Normal API data
api_version = int(main_config.get('ADSBX', 'API_VERSION'))
# Normal API data
api_version = int(main_config.get("ADSBX", "API_VERSION"))
if api_version == 2:
icao_key = 'hex'
icao_key = "hex"
elif api_version == 1:
icao_key = 'icao'
icao_key = "icao"
else:
raise ValueError("Invalid API Version")
from defADSBX import pull_adsbx

data = pull_adsbx(planes)
if data is not None:
if data['ac'] is not None:
if data["ac"] is not None:
data_indexed = {}
for planeData in data['ac']:
for planeData in data["ac"]:
data_indexed[planeData[icao_key].upper()] = planeData
for key, obj in planes.items():
try:
Expand All @@ -152,6 +192,7 @@ def service_exit(signum, frame):
failed_count += 1
elif source == "OPENS":
from defOpenSky import pull_opensky

planeData, failed = pull_opensky(planes)
if failed == False:
if planeData != None and planeData.states != []:
Expand All @@ -170,46 +211,68 @@ def service_exit(signum, frame):
obj.run_empty()
elif failed:
failed_count += 1
if failed_count >= 10 and main_config.getboolean('DATA', 'FAILOVER'):
if failed_count >= 10 and main_config.getboolean("DATA", "FAILOVER"):
if source == "OPENS":
source = "ADSBX"
elif source == "ADSBX":
source = "OPENS"
failed_count = 0
if main_config.getboolean('DISCORD', 'ENABLE'):
if main_config.getboolean("DISCORD", "ENABLE"):
from defDiscord import sendDis

sendDis(str("Failed over to " + source), main_config)
elapsed_calc_time = time.time() - start_time
datetime_tz = datetime.now(tz)
footer = "-------- " + str(running_Count) + " -------- " + str(datetime_tz.strftime("%I:%M:%S %p")) + " ------------------------Elapsed Time- " + str(round(elapsed_calc_time, 3)) + " -------------------------------------"
print (Back.GREEN + Fore.BLACK + footer[0:100] + Style.RESET_ALL)
footer = (
"-------- "
+ str(running_Count)
+ " -------- "
+ str(datetime_tz.strftime("%I:%M:%S %p"))
+ " ------------------------Elapsed Time- "
+ str(round(elapsed_calc_time, 3))
+ " -------------------------------------"
)
print(Back.GREEN + Fore.BLACK + footer[0:100] + Style.RESET_ALL)

sleep_sec = 30
for i in range(sleep_sec,0,-1):
for i in range(sleep_sec, 0, -1):
if i < 10:
i = " " + str(i)
sys.stdout.write("\r")
sys.stdout.write(Back.RED + "Sleep {00000000}".format(i) + Style.RESET_ALL)
sys.stdout.flush()
time.sleep(1)
sys.stdout.write(Back.RED + ('\x1b[1K\r' +"Slept for " +str(sleep_sec)) + Style.RESET_ALL)
sys.stdout.write(
Back.RED + ("\x1b[1K\r" + "Slept for " + str(sleep_sec)) + Style.RESET_ALL
)
print()
except KeyboardInterrupt as e:
print(e)
if main_config.getboolean('DISCORD', 'ENABLE'):
if main_config.getboolean("DISCORD", "ENABLE"):
from defDiscord import sendDis

sendDis(str("Manual Exit: " + str(e)), main_config)
except Exception as e:
if main_config.getboolean('DISCORD', 'ENABLE'):
if main_config.getboolean("DISCORD", "ENABLE"):
try:
os.remove('crash_latest.log')
os.remove("crash_latest.log")
except OSError:
pass
import logging
logging.basicConfig(filename='crash_latest.log', filemode='w', format='%(asctime)s - %(message)s')

logging.basicConfig(
filename="crash_latest.log",
filemode="w",
format="%(asctime)s - %(message)s",
)
logging.Formatter.converter = time.gmtime
logging.error(e)
logging.error(str(traceback.format_exc()))
from defDiscord import sendDis
sendDis(str("Error Exiting: " + str(e) + "Failed on " + key), main_config, "crash_latest.log")
raise e

sendDis(
str("Error Exiting: " + str(e) + "Failed on " + key),
main_config,
"crash_latest.log",
)
raise e
Loading