Browse Source

Merge branch 'multi' into patch-4

pull/55/head
Jack Sweeney 2 years ago committed by GitHub
parent
commit
b08c0a195c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 42
      Dockerfile
  3. BIN
      ExImages/DiscordEX2.png
  4. BIN
      ExImages/DiscordEX3.png
  5. BIN
      ExImages/PushbulletEX.png
  6. 3
      Pipfile
  7. 910
      Pipfile.lock
  8. 2
      PseudoCode.md
  9. 23
      README.md
  10. 82
      __main__.py
  11. 478
      aircraft_type_fuel_consumption_rates.json
  12. 3
      calculate_headings.py
  13. 23
      configs/mainconf.ini.example
  14. 18
      configs/plane1.ini.example
  15. 8
      defAirport.py
  16. 39
      defMastodon.py
  17. 4
      defOpenSky.py
  18. 34
      defRpdADSBX.py
  19. 71
      defSS.py
  20. 56
      defTelegram.py
  21. 1
      docker-compose.yml
  22. 15
      fuel_calc.py
  23. 5
      modify_image.py
  24. 130
      planeClass.py
  25. 22
      requirements.txt

3
.gitignore vendored

@ -1,3 +1,5 @@
configs/*.ini
# PyCharm
.idea
.vscode/settings.json
@ -9,3 +11,4 @@ lookup_route.py
icao_url_gen.py
install.sh
coul_icao_gen.py
test.py

42
Dockerfile

@ -1,23 +1,37 @@
FROM python:3
WORKDIR /plane-notify
USER root
COPY . .
# Set the Chrome repo.
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list
# Install Chrome.
RUN apt-get update && apt-get -y install google-chrome-stable
# Add pipenv
RUN pip install pipenv==2021.5.29
# Install dependencies
RUN pipenv install
RUN set -ex && \
apt-get update -qq && \
apt-get -y -qq install --no-install-recommends \
ca-certificates \
gnupg && \
curl -sSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list && \
apt-get -y -qq update \
&& apt-get -y -qq install --no-install-recommends \
bash \
curl \
google-chrome-stable \
python3 \
python3-dev \
python3-pip \
python3-setuptools \
python3-wheel \
&& rm -rf \
/var/lib/apt/lists/* \
/var/cache/apt/archives
RUN pip3 install --upgrade pip && \
pip3 install -U --no-cache-dir -r ./requirements.txt
# Added needed folder for plane-notify process
RUN mkdir /home/plane-notify
RUN mkdir -p /home/plane-notify
CMD pipenv run python /plane-notify/__main__.py
CMD python3 /plane-notify/__main__.py

BIN
ExImages/DiscordEX2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
ExImages/DiscordEX3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
ExImages/PushbulletEX.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

3
Pipfile

@ -12,7 +12,6 @@ tabulate = "*"
pytz = "*"
pillow = "*"
tweepy = "*"
"pushbullet.py" = "*"
discord-webhook = "*"
selenium = "*"
opensky-api = {editable = true, git = "https://github.com/openskynetwork/opensky-api.git", subdirectory = "python"}
@ -27,5 +26,7 @@ lxml = "*"
beautifulsoup4 = "*"
python-telegram-bot = "*"
configparser = "*"
"mastodon.py" = "*"
[requires]
python_version = "3.9"

910
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

2
PseudoCode.md

@ -4,5 +4,5 @@
- 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.
- A Static map image is created based off location name. (Google Static Maps API) or a screenshot of <https://globe.theairtraffic.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 can all be setup and enabled in each plane's config file). Outputs the location name, map, image, and flight time on landing. (Tweepy and "Pushbullet.py" and Discord_webhooks)

23
README.md

@ -3,11 +3,11 @@
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/c4e1d839eec3468cadfe351d64dc1ac4)](https://app.codacy.com/manual/Jxck-S/plane-notify?utm_source=github.com&utm_medium=referral&utm_content=Jxck-S/plane-notify&utm_campaign=Badge_Grade_Settings)
[![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)](https://opensource.org/licenses/)
Notify if configured planes have taken off or landed using Python with <a href="https://opensky-network.org/">OpenSky</a>(free) or <a href="https://www.adsbexchange.com/">ADSBExchange</a> Data(paid but much better), outputs location of takeoff location of landing and takeoff by reverse lookup of coordinates.
Notify if configured planes have taken off or landed using Python with <a href="https://opensky-network.org/">OpenSky</a>(free) or <a href="https://www.adsbexchange.com/">ADSBExchange</a> Data(paid, declining data, and run by clowns), outputs location of takeoff location of landing and takeoff by reverse lookup of coordinates.
### Discord Output Example
![Discord Output Example](./ExImages/DiscordEX.png?raw=true)
![Discord Output Example](./ExImages/DiscordEX2.png?raw=true)
#### More examples are in the ExImages folder
@ -15,15 +15,16 @@ Notify if configured planes have taken off or landed using Python with <a href="
### Background
I made this program so I could track Elon Musk's Jet and share with others of his whereabouts on Twitter. [![Twitter Follow](https://img.shields.io/twitter/follow/ElonJet.svg?style=social)](https://twitter.com/ElonJet) I have now Expanded and run multiple accounts for multiple planes, a list of the accounts here [plane-notify Twitter List](https://twitter.com/i/lists/1307414615316467715)
I made this program so I could track Elon Musk's Jet and share his whereabouts with others orginally on Twitter (but now suspended, but now also on other platforms). I have now expanded and run multiple accounts for multiple planes, a list of the accounts can be found here <https://grndcntrl.net/links>
### Contributing
I'm open to any help or suggestions, I realize there are many better ways to improve this program and better ways to get this program to work properly, Im only a noob. I'll accept pull requests. If you'd like to discuss join <https://JacksTech.net/Discord>
I'm open to any help or suggestions, I realize there are many better ways to improve this program and better ways to get this program to work properly, I'm only a noob. I'll accept pull requests. If you'd like to discuss join <https://discord.gg/groundcontrol>
### [Algorithm](PseudoCode.md)
## Setup / Install
- Install using the following steps or use Docker, scroll down to the Docker section.
### Make sure Python/PIP is installed
@ -42,12 +43,15 @@ pipenv install
### Install Selenium / ChromeDriver or setup Google Static Maps
Selenium/ChromeDriver is used to take a screenshot of the plane on globe.adsbexchange.com. Or use Google Static Maps, which can cost money if overused(No tutorial use <https://developers.google.com/maps/documentation/maps-static/get-api-key> to get to a key).
#### Chromium
Selenium/ChromeDriver is used to take a screenshot of the plane on globe.theairtraffic.com. Or use Google Static Maps, which can cost money if overused(No tutorial use <https://developers.google.com/maps/documentation/maps-static/get-api-key> to get to a key).
#### Chrome
- This is assuming linux/debian
```bash
sudo apt-get install chromium
curl -sSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
apt update
apt install google-chrome-stable
```
These output methods once installed can be configured in the planes config you create, using the example plane1.ini
@ -67,6 +71,7 @@ cd plane-notify
### Configure main config file with keys and URLs (mainconf.ini) in the configs directory
- Copy `mainconf.ini.example` to `mainconf.ini` andCopy `plane1.ini.example` to `plane1.ini`. `plane1.ini` can change names as long as it ends with the ini extension
- 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 you are ready to pay.
@ -91,7 +96,7 @@ screen -R <name screen whatever you want>
### Start Program
```bash
pipenv run python __main__
pipenv run python __main__.py
```
## Using with Docker

82
__main__.py

@ -1,16 +1,21 @@
import configparser
from logging import DEBUG
import time
from colorama import Fore, Back, Style
import platform
import traceback
import os
if platform.system() == "Windows":
from colorama import init
init(convert=True)
elif platform.system() == "Linux":
if os.path.exists("/tmp/plane-notify"):
import shutil
shutil.rmtree("/tmp/plane-notify")
os.makedirs("/tmp/plane-notify")
os.makedirs("/tmp/plane-notify/chrome")
from planeClass import Plane
from datetime import datetime
import pytz
import os
import signal
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
@ -20,7 +25,11 @@ sys.path.extend([os.getcwd()])
#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'),
('Mictronics_db.zip', "https://www.mictronics.de/aircraft-database/indexedDB.php")]
for file in required_files:
file_name = file[0]
url = file[1]
@ -49,11 +58,13 @@ main_config.read('./configs/mainconf.ini')
source = main_config.get('DATA', 'SOURCE')
if main_config.getboolean('DISCORD', 'ENABLE'):
from defDiscord import sendDis
role_id = main_config.get('DISCORD', 'ROLE_ID') if main_config.has_option('DISCORD', 'ROLE_ID') and main_config.get('DISCORD', 'ROLE_ID').strip() != "" else None
sendDis("Started", main_config, role_id = main_config.get('DISCORD', 'ROLE_ID'))
def service_exit(signum, frame):
if main_config.getboolean('DISCORD', 'ENABLE'):
from defDiscord import sendDis
sendDis("Service Stop", main_config, role_id = main_config.get('DISCORD', 'ROLE_ID'))
role_id = main_config.get('DISCORD', 'ROLE_ID') if main_config.has_option('DISCORD', 'ROLE_ID') and main_config.get('DISCORD', 'ROLE_ID').strip() != "" else None
sendDis("Service Stop", main_config, role_id = role_id)
raise SystemExit("Service Stop")
signal.signal(signal.SIGTERM, service_exit)
if os.path.isfile("lookup_route.py"):
@ -69,7 +80,7 @@ try:
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:
if "disabled" not in dirpath:
print(os.path.join(dirpath, filename))
plane_config = configparser.ConfigParser()
plane_config.read((os.path.join(dirpath, filename)))
@ -140,18 +151,66 @@ try:
for planeData in data['ac']:
data_indexed[planeData[icao_key].upper()] = planeData
for key, obj in planes.items():
if key in data_indexed.keys():
try:
if api_version == 1:
obj.run_adsbx_v1(data_indexed[key.upper()])
elif api_version == 2:
obj.run_adsbx_v2(data_indexed[key.upper()])
else:
except KeyError:
obj.run_empty()
else:
for obj in planes.values():
obj.run_empty()
else:
failed_count += 1
elif source == "RpdADSBX":
#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:
# with open('./testing/acastest.json') as f:
# data = f.readlines()
# ras += data
ra_count = len(ras)
if last_ra_count is not None and ra_count != last_ra_count:
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]
else:
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])
obj.expire_ra_types()
from defRpdADSBX import pull_rpdadsbx
data_indexed = {}
for icao in planes:
plane = planes[icao]
plane_info = pull_rpdadsbx(icao)
if plane_info:
if plane_info['ac']:
data_indexed[icao.upper()] = plane_info['ac'][0]
plane.run_adsbx_v2(data_indexed[icao.upper()])
else:
plane.run_empty()
else:
print(f"No data for icao {icao}. Skipping...")
plane.run_empty()
if not data_indexed:
failed_count += 1
elif source == "OPENS":
from defOpenSky import pull_opensky
planeData, failed = pull_opensky(planes)
@ -186,7 +245,10 @@ try:
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
if main_config.has_section('SLEEP'):
sleep_sec = int(main_config.get('SLEEP', 'SLEEPSEC'))
else:
sleep_sec = 30
for i in range(sleep_sec,0,-1):
if i < 10:
i = " " + str(i)
@ -208,10 +270,10 @@ except Exception as e:
except OSError:
pass
import logging
logging.basicConfig(filename='crash_latest.log', filemode='w', format='%(asctime)s - %(message)s',level=logging.DEBUG)
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 " + "https://globe.adsbexchange.com/?icao=" + key), main_config, main_config.get('DISCORD', 'ROLE_ID'), "crash_latest.log")
sendDis(str("Error Exiting: " + str(e) + f"Failed on ({obj.config_path}) https://globe.theairtraffic.com/?icao={key} "), main_config, main_config.get('DISCORD', 'ROLE_ID'), "crash_latest.log")
raise e

478
aircraft_type_fuel_consumption_rates.json

@ -1,214 +1,412 @@
{
"EA50": {
"name": "Eclipse 550",
"galph": 76,
"category": "VLJ"
"name": "Eclipse 550",
"galph": 76,
"category": "VLJ"
},
"LJ31": {
"name": "Learjet 31",
"galph": 202,
"category": "Light"
"name": "Learjet 31",
"galph": 202,
"category": "Light"
},
"LJ40": {
"name": "Learjet 40",
"galph": 207,
"category": "Light"
"name": "Learjet 40",
"galph": 207,
"category": "Light"
},
"PC24": {
"name": "Pilatus PC-24",
"galph": 154,
"category": "Light"
"name": "Pilatus PC-24",
"galph": 154,
"category": "Light"
},
"LJ45": {
"name": "Learjet 45",
"galph": 205,
"category": "Super Light"
"name": "Learjet 45",
"galph": 205,
"category": "Super Light"
},
"LJ70": {
"name": "Learjet 70",
"galph": 198,
"category": "Super Light"
"name": "Learjet 70",
"galph": 198,
"category": "Super Light"
},
"LJ75": {
"name": "Learjet 75",
"galph": 199,
"category": "Super Light"
"name": "Learjet 75",
"galph": 199,
"category": "Super Light"
},
"G150": {
"name": "Gulfstream G150",
"galph": 228,
"category": "Midsize"
"name": "Gulfstream G150",
"galph": 228,
"category": "Midsize"
},
"LJ60": {
"name": "Learjet 60",
"galph": 239,
"category": "Midsize"
"name": "Learjet 60",
"galph": 239,
"category": "Midsize"
},
"GALX": {
"name": "Gulfstream G200",
"galph": 278,
"category": "Super Midsize"
"name": "Gulfstream G200",
"galph": 278,
"category": "Super Midsize"
},
"G280": {
"name": "Gulfstream G280",
"galph": 297,
"category": "Super Midsize"
"name": "Gulfstream G280",
"galph": 297,
"category": "Super Midsize"
},
"GLF5": {
"name": "Gulfstream G500",
"galph": 447,
"category": "Large"
"name": "Gulfstream G500",
"galph": 447,
"category": "Large"
},
"GLF6": {
"name": "Gulfstream G650",
"galph": 503,
"category": "Ultra Long Range"
"name": "Gulfstream G650",
"galph": 503,
"category": "Ultra Long Range"
},
"PC12": {
"name": "Pilatus PC-12",
"galph": 66,
"category": "Turboprop Aircraft"
"name": "Pilatus PC-12",
"galph": 66,
"category": "Turboprop Aircraft"
},
"GLEX": {
"name": "Global",
"galph": 500,
"category": "Ultra Long Range"
}
,
"name": "Global",
"galph": 500,
"category": "Ultra Long Range"
},
"CL30": {
"name": "Challenger 300",
"galph": 295,
"category": "Super Midsize"
}, "B742": {
"name": "Boeing 747-200",
"galph": 3830,
"category": "Large"
}, "T38": {
"name": "T-38 Talon",
"galph": 375,
"category": "Fighter"
}, "WB57": {
"name": "Martin B-57 Canberra",
"galph": 531,
"category": "Twinjet Tactical Bomber and Reconnaissance"
}, "B74S": {
"name": "747 SP",
"galph": 2289,
"category": "Large"
}, "B752": {
"name": "757 200",
"galph": 877,
"category": "Large"
},
"B738": {
"name": "737 800",
"galph": 832,
"category": "Medium"
"name": "Challenger 300",
"galph": 295,
"category": "Super Midsize"
},
"B742": {
"name": "Boeing 747-200",
"galph": 3830,
"category": "Large"
},
"T38": {
"name": "T-38 Talon",
"galph": 375,
"category": "Fighter"
},
"WB57": {
"name": "Martin B-57 Canberra",
"galph": 531,
"category": "Twinjet Tactical Bomber and Reconnaissance"
},
"B74S": {
"name": "747 SP",
"galph": 2289,
"category": "Large"
},
"B752": {
"name": "757 200",
"galph": 877,
"category": "Large"
},
"B738": {
"name": "737 800",
"galph": 832,
"category": "Medium"
},
"B737": {
"name": "737 700",
"galph": 796,
"category": "Medium"
"name": "737 700",
"galph": 796,
"category": "Medium"
},
"A320": {
"name": "A320",
"galph": 800,
"category": "Medium"
"name": "A320",
"galph": 800,
"category": "Medium"
},
"P3": {
"name": "Lockheed Orion P3",
"galph": 671,
"category": "Turboprop"
"name": "Lockheed Orion P3",
"galph": 671,
"category": "Turboprop"
},
"C750": {
"name": "Cessna 750 Citation X",
"galph": 347,
"category": "Small Private Jet"
"name": "Cessna 750 Citation X",
"galph": 347,
"category": "Small Private Jet"
},
"FA7X": {
"name": "Dassult Falcon 7X",
"galph": 380,
"category": "Small Private Jet"
"name": "Dassult Falcon 7X",
"galph": 380,
"category": "Small Private Jet"
},
"F900": {
"name": "Dassult Falcon 900",
"galph": 347,
"category": "Small Private Jet"
"name": "Dassult Falcon 900",
"galph": 347,
"category": "Small Private Jet"
},
"H25B": {
"name": "Hawker 750/850",
"galph": 270,
"category": "Small Private Jet"
"name": "Hawker 750/850",
"galph": 270,
"category": "Small Private Jet"
},
"C680": {
"name": "Cessna 680 Citation",
"galph": 247,
"category": "Small Private Jet"
"name": "Cessna 680 Citation",
"galph": 247,
"category": "Small Private Jet"
},
"GLF3": {
"name": "Gulfstream 3",
"galph": 568,
"category": "Heavy Private Jet"
},
"name": "Gulfstream 3",
"galph": 568,
"category": "Heavy Private Jet"
},
"GLF4": {
"name": "Gulfstream 4",
"galph": 479,
"category": "Heavy Private Jet"
"name": "Gulfstream 4",
"galph": 479,
"category": "Heavy Private Jet"
},
"CL60": {
"name": "Bombardier CL-600 Challenge",
"galph": 262,
"category": "Mid-size Private Jet"
"name": "Bombardier CL-600 Challenge",
"galph": 262,
"category": "Mid-size Private Jet"
},
"A139": {
"name": "Agusta-Bell AW139",
"galph": 150,
"category": "Medium Utility Helicopter"
"name": "Agusta-Bell AW139",
"galph": 150,
"category": "Medium Utility Helicopter"
},
"GL5T": {
"name": "Global 5000",
"galph": 455,
"category": "Heavy Private Jet"
"name": "Global 5000",
"galph": 455,
"category": "Heavy Private Jet"
},
"GA6C": {
"name": "Gulfstream G600",
"galph": 458,
"category": "Heavy Private Jet"
"name": "Gulfstream G600",
"galph": 458,
"category": "Heavy Private Jet"
},
"A337": {
"name": "Airbus Beluga XL",
"galph": 1800,
"category": "Large Transport Aircraft"
"name": "Airbus Beluga XL",
"galph": 1800,
"category": "Large Transport Aircraft"
},
"A3ST": {
"name": "Airbus Beluga",
"galph": 1260,
"category": "Large Transport Aircraft"
"name": "Airbus Beluga",
"galph": 1260,
"category": "Large Transport Aircraft"
},
"F2TH": {
"name": "Dassault Falcon 2000",
"galph": 245,
"category": "Medium Private Jet"
"name": "Dassault Falcon 2000",
"galph": 245,
"category": "Medium Private Jet"
},
"GA5C": {
"name": "Gulfstream G500",
"galph": 402,
"category": "Large Private Jet"
"name": "Gulfstream G500",
"galph": 402,
"category": "Large Private Jet"
},
"C130": {
"name": "Lockheed C130",
"galph": 740,
"category": "Medium Cargo"
"name": "Lockheed C130",
"galph": 740,
"category": "Medium Cargo"
},
"B762": {
"name": "Boeing 767 200",
"galph": 1722,
"category": "Wide-body"
"name": "Boeing 767 200",
"galph": 1722,
"category": "Wide-body"
},
"B772": {
"name": "Boeing 777 200",
"galph": 2300,
"category": "Wide-body"
"name": "Boeing 777 200",
"galph": 2300,
"category": "Wide-body"
},
"SLCH": {
"name": "Stratolaunch",
"galph": 2396,
"category": "Special"
},
"P51": {
"name": "P51 Mustang",
"galph": 65,
"category": "Fighter"
},
"HDJT": {
"name": "Honda Jet",
"galph": 90,
"category": "Light Jet"
},
"B744": {
"name": "Boeing 747-400",
"galph": 3700,
"category": "Heavy Airliner"
},
"E190": {
"name": "Embrar E190",
"galph": 469,
"category": "Heavy Jet"
},
"FA50": {
"name": "Falcon 50",
"galph": 229,
"category": "Heavy Jet"
},
"GL7T": {
"name": "Global 7000",
"galph": 460,
"category": "Heavy Jet"
},
"GL6T": {
"name": "",
"galph": 455.0,
"category": ""
},
"C68A": {
"name": "",
"galph": 212.0,
"category": ""
},
"C56X": {
"name": "",
"galph": 217.0,
"category": ""
},
"B763": {
"name": "",
"galph": 1320.0,
"category": ""
},
"A310": {
"name": "",
"galph": 1189.0,
"category": ""
},
"A330": {
"name": "",
"galph": 1505.0,
"category": ""
},
"A380": {
"name": "",
"galph": 4062.0,
"category": ""
},
"E170": {
"name": "",
"galph": 469.0,
"category": ""
},
"DC87": {
"name": "",
"galph": 1250.0,
"category": ""
},
"SGUP": {
"name": "",
"galph": 1156.0,
"category": ""
},
"WHK2": {
"name": "",
"galph": 500.0,
"category": ""
},
"B350": {
"name": "",
"galph": 122.0,
"category": ""
},
"BE30": {
"name": "",
"galph": 121.0,
"category": ""
},
"FA8X": {
"name": "",
"galph": 380.0,
"category": ""
},
"E550": {
"name": "",
"galph": 280.0,
"category": ""
},
"E55P": {
"name": "",
"galph": 166.0,
"category": ""
},
"A332": {
"name": "",
"galph": 1480,
"category": ""
},
"GA7C": {
"name": "",
"galph": 382.0,
"category": ""
},
"FA6X": {
"name": "",
"galph": 419.0,
"category": ""
},
"B3XM": {
"name": "",
"galph": 716.0,
"category": ""
},
"B779": {
"name": "",
"galph": 2250.0,
"category": ""
},
"BE22": {
"name": "",
"galph": 60.0,
"category": ""
},
"C560": {
"name": "",
"galph": 182.0,
"category": ""
},
"E145": {
"name": "",
"galph": 284.0,
"category": ""
},
"C25C": {
"name": "",
"galph": 110.0,
"category": ""
},
"C25B": {
"name": "",
"galph": 110.0,
"category": ""
},
"C441": {
"name": "",
"galph": 57.0,
"category": ""
},
"E50P": {
"name": "",
"galph": 109.0,
"category": ""
},
"CRJ2": {
"name": "",
"galph": 325.0,
"category": ""
},
"CRJ7": {
"name": "",
"galph": 444.0,
"category": ""
},
"BE40": {
"name": "",
"galph": 220.0,
"category": ""
},
"C700": {
"name": "",
"galph": 288.0,
"category": ""
}
}

3
calculate_headings.py

@ -18,6 +18,9 @@ def calculate_cardinal(d):
return card
def calculate_deg_change(new_heading, original_heading):
"""Calculates change between two headings, returns negative degree if change is left, positive if right"""
if new_heading is None:
print("Track heading missing. No change")
return 0
normal = abs(original_heading-new_heading)
across_inital = 360 - abs(original_heading-new_heading)
if across_inital < normal:

23
configs/mainconf.ini → configs/mainconf.ini.example

@ -2,8 +2,8 @@
#Source to pull data from
#SHOULD BE ADSBX which is ADS-B Exchange or OPENS which is OpenSky
#By default configured with OpenSky which anyone can use without a login
#ADS-B Exchange has better data but is not avalible unless you pay (see: https://www.adsbexchange.com/data/ )
SOURCE = OPENS
#ADS-B Exchange has better data but is not available unless you feed their network or pay.
SOURCE = RpdADSBX
#Default amount of time after data loss to trigger a landing when under 10k ft
DATA_LOSS_MINS = 5
#Failover from one source to the other, only enable if you have both sources setup.
@ -20,7 +20,7 @@ API_VERSION = 1
#ADSBX API Proxy, https://gitlab.com/jjwiseman/adsbx-api-proxy, v2 input, v1 or v2 output from proxy
ENABLE_PROXY = FALSE
#Full URL http://host:port
PROXY_HOST =
PROXY_HOST =
#OpenSky https://opensky-network.org/apidoc/index.html
#When using without your own login user and pass should be None
@ -28,15 +28,25 @@ PROXY_HOST =
USERNAME = None
PASSWORD = None
#ADS-B Exchange on RapidAPI https://rapidapi.com/adsbx/api/adsbexchange-com1/
[RpdADSBX]
API_KEY = none
API_VERSION = 2
#Define the delay interval in seconds between each data request. This is useful if you have limited requests in the API.
[SLEEP]
SLEEPSEC = 60
[GOOGLE]
#API KEY for Google Static Maps only if you using this on any of the planes.
API_KEY = googleapikey
#Used for failover messages and program exits notifcation
#Used for failover messages and program exits notification
[DISCORD]
ENABLE = FALSE
USERNAME = usernamehere
URL = webhookurl
ROLE_ID =
[TFRS]
URL = http://127.0.0.1:5000/detailed_all
@ -47,3 +57,8 @@ ENABLE = False
ENABLE = False
CONSUMER_KEY = ck
CONSUMER_SECRET = cs
[MAP]
#Map to create from Google Static Maps or screenshot global tar1090 from globe.theairtraffic.com
#Enter GOOGLESTATICMAP or ADSBX
OPTION = ADSBX

18
configs/plane1.ini → configs/plane1.ini.example

@ -9,11 +9,11 @@ ICAO = icaohere
; OVERRIDE_TYPELONG =
; OVERRIDE_OWNER =
; DATA_LOSS_MINS = 20
; CONCEAL_AC_ID = True
; CONCEAL_PIA = False
[MAP]
#Map to create from Google Static Maps or screenshot global tar1090 from globe.adsbexchange.com
#Enter GOOGLESTATICMAP or ADSBX
OPTION = ADSBX
#Optional, map selection moved to mainconf, this is for map overlays per plane
#Tar1090 overlays option, should be seperated by comma no space, remove option all together to disable any
OVERLAYS = nexrad
@ -29,12 +29,6 @@ TITLE =
ACCESS_TOKEN = athere
ACCESS_TOKEN_SECRET = atshere
[PUSHBULLET]
ENABLE = FALSE
TITLE = Title Of Pushbullet message
API_KEY = apikey
CHANNEL_TAG = channeltag
[DISCORD]
ENABLE = FALSE
#WEBHOOK URL https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks
@ -56,3 +50,9 @@ ENABLE = FALSE
TITLE = Title Of Telegram message
ROOM_ID = -100xxxxxxxxxx
BOT_TOKEN = xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
[MASTODON]
ENABLE = TRUE
ACCESS_TOKEN = mastodonaccesstoken
APP_URL = mastodonappurl

8
defAirport.py

@ -30,6 +30,7 @@ def getClosestAirport(latitude, longitude, allowed_types):
return closest_airport_dict
def get_airport_by_icao(icao):
with open('./dependencies/airports.csv', 'r', encoding='utf-8') as airport_csv:
matching_airport = None
airport_csv_reader = csv.DictReader(filter(lambda row: row[0]!='#', airport_csv))
for airport in airport_csv_reader:
if airport['gps_code'] == icao:
@ -37,5 +38,8 @@ def get_airport_by_icao(icao):
#Convert indent key to icao key as its labeled icao in other places not ident
matching_airport['icao'] = matching_airport.pop('gps_code')
break
matching_airport = add_airport_region(matching_airport)
return matching_airport
if matching_airport:
matching_airport = add_airport_region(matching_airport)
return matching_airport
else:
return None

39
defMastodon.py

@ -0,0 +1,39 @@
def sendMastodon(photo, message, config):
from mastodon import Mastodon
sent = False
retry_c = 0
while sent == False:
try:
bot = Mastodon(
access_token=config.get('MASTODON','ACCESS_TOKEN'),
api_base_url=config.get('MASTODON','APP_URL')
)
mediaid = bot.media_post(photo, mime_type="image/jpeg")
sent = bot.status_post(message,None,mediaid,False, "Public")
except Exception as err:
print('err.args:')
print(err.args)
print(f"Unexpected {err=}, {type(err)=}")
print("\nString err:\n"+str(err))
if retry_c > 4:
print('Mastodon attempts exceeded. Message not sent.')
break
elif str(err) == 'Unauthorized':
print('Invalid Mastodon bot token, message not sent.')
break
elif str(err) == 'Timed out':
retry_c += 1
print('Mastodon timeout count: '+str(retry_c))
pass
elif str(err)[:35] == '[Errno 2] No such file or directory':
print('Mastodon module couldn\'t find an image to send.')
break
elif str(err) == 'Media_caption_too_long':
print('Mastodon image caption lenght exceeds 1024 characters. Message not send.')
break
else:
print('[X] Unknown error. Message not sent.')
break
else:
print("Mastodon message successfully sent.")
return sent

4
defOpenSky.py

@ -4,7 +4,9 @@ def pull_opensky(planes):
main_config.read('./configs/mainconf.ini')
from opensky_api import OpenSkyApi
planeData = None
opens_api = OpenSkyApi(username= None if main_config.get('OPENSKY', 'USERNAME').upper() == "NONE" else main_config.get('OPENSKY', 'USERNAME'), password= None if main_config.get('OPENSKY', 'PASSWORD').upper() == "NONE" else main_config.get('OPENSKY', 'PASSWORD').upper())
opens_api = OpenSkyApi(
username= None if main_config.get('OPENSKY', 'USERNAME').upper() == "NONE" else main_config.get('OPENSKY', 'USERNAME'),
password= None if main_config.get('OPENSKY', 'PASSWORD').upper() == "NONE" else main_config.get('OPENSKY', 'PASSWORD'))
failed = False
icao_array = []
for key in planes.keys():

34
defRpdADSBX.py

@ -0,0 +1,34 @@
import requests
import configparser
from datetime import datetime
main_config = configparser.ConfigParser()
main_config.read('./configs/mainconf.ini')
api_version = main_config.get('RpdADSBX', 'API_VERSION')
def pull_rpdadsbx(planes):
api_version = int(main_config.get('RpdADSBX', 'API_VERSION'))
if api_version != 2:
raise ValueError("Bad RapidAPI ADSBX API Version")
url = "https://adsbexchange-com1.p.rapidapi.com/v2/icao/" + planes + "/"
headers = {
"X-RapidAPI-Host": "adsbexchange-com1.p.rapidapi.com",
"X-RapidAPI-Key": main_config.get('RpdADSBX', 'API_KEY')
}
try:
response = requests.get(url, headers = headers, timeout=30)
response.raise_for_status()
data = response.json()
if "msg" in data.keys() and data['msg'] != "No error":
raise ValueError("Error from ADSBX: msg = ", data['msg'])
if "ctime" in data.keys():
data_ctime = float(data['ctime']) / 1000.0
print("Data ctime:",datetime.utcfromtimestamp(data_ctime))
if "now" in data.keys():
data_now = float(data['now']) / 1000.0
print("Data now time:",datetime.utcfromtimestamp(data_now))
print("Current UTC:", datetime.utcnow())
return data
except Exception as e:
print('Error calling RapidAPI', e)
return None

71
defSS.py

@ -6,31 +6,41 @@ import time
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException
def get_adsbx_screenshot(file_path, url_params, enable_labels=False, enable_track_labels=False, overrides={}):
def blur_elements_by_id(browser, element_ids):
for element in element_ids:
try:
element = browser.find_element(By.ID, element)
browser.execute_script("arguments[0].style.filter = 'blur(7px)';", element)
except NoSuchElementException:
print("Issue finding:", element, "on page")
def get_adsbx_screenshot(file_path, url_params, enable_labels=False, enable_track_labels=False, overrides={}, conceal_ac_id=False, conceal_pia=False):
import os
import platform
chrome_options = webdriver.ChromeOptions()
chrome_options.headless = True
chrome_options.add_argument('window-size=800,800')
chrome_options.add_argument('ignore-certificate-errors')
#Plane images issue loading when in headless setting agent fixes.
chrome_options.add_experimental_option('excludeSwitches', ['enable-logging'])
if platform.system() == "Linux":
chrome_options.add_argument('crash-dumps-dir=/tmp/plane-notify/chrome')
#Plane images issue loading when in headless setting agent fixes.
chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36")
import os
import platform
if platform.system() == "Linux" and os.geteuid()==0:
chrome_options.add_argument('--no-sandbox') # required when running as root user. otherwise you would get no sandbox errors.
browser = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)
url = f"https://globe.adsbexchange.com/?{url_params}"
print(url)
url = f"https://globe.theairtraffic.com/?{url_params}"
print(f"Getting Screenshot of {url}")
browser.set_page_load_timeout(80)
browser.get(url)
WebDriverWait(browser, 40).until(lambda d: d.execute_script("return jQuery.active == 0"))
remove_id_elements = ["show_trace", "credits", 'infoblock_close', 'selected_photo_link', "history_collapse"]
for element in remove_id_elements:
try:
element = browser.find_element_by_id(element)
element = browser.find_element(By.ID, element)
browser.execute_script("""var element = arguments[0]; element.parentNode.removeChild(element); """, element)
except:
print("issue removing", element, "from map")
print("Issue finding:", element, "on page")
#Remove watermark on data
try:
browser.execute_script("document.getElementById('selected_infoblock').className = 'none';")
@ -43,41 +53,45 @@ def get_adsbx_screenshot(file_path, url_params, enable_labels=False, enable_trac
print("Couldn't disable sidebar on map")
#Remove Google Ads
try:
element = browser.find_element_by_xpath("//*[contains(@id, 'FIOnDemandWrapper_')]")
element = browser.find_element(By.XPATH, "//*[contains(@id, 'FIOnDemandWrapper_')]")
browser.execute_script("""var element = arguments[0]; element.parentNode.removeChild(element); """, element)
except:
print("Couldn't remove Google Ads")
#Remove share
# try:
# element = browser.find_element_by_xpath("//*[contains(text(), 'Copy Link')]")
# browser.execute_script("""var element = arguments[0]; element.parentNode.removeChild(element); """, element)
# except Exception as e:
# print("Couldn't remove share button from map", e)
#Remove Copy Link
try:
element = browser.find_element(By.XPATH, "//*[@id='selected_icao']/span[2]/a")
browser.execute_script("""var element = arguments[0]; element.parentNode.removeChild(element); """, element)
except Exception as e:
print("Couldn't remove copy link button from map", e)
#browser.execute_script("toggleFollow()")
if conceal_pia or conceal_ac_id:
blur_elements_by_id(browser, ["selected_callsign", "selected_icao", "selected_squawk1"])
if conceal_ac_id:
blur_elements_by_id(browser, ["selected_registration", "selected_country", "selected_dbFlags", "selected_ownop", "selected_typelong", "selected_icaotype", "airplanePhoto", "silhouette", "copyrightInfo"])
if enable_labels:
browser.find_element_by_tag_name('body').send_keys('l')
browser.find_element(By.TAG_NAME, 'body').send_keys('l')
if enable_track_labels:
browser.find_element_by_tag_name('body').send_keys('k')
browser.find_element(By.TAG_NAME, 'body').send_keys('k')
from selenium.webdriver.support import expected_conditions as EC
time.sleep(15)
if 'reg' in overrides.keys():
element = browser.find_element_by_id("selected_registration")
element = browser.find_element(By.ID, "selected_registration")
browser.execute_script(f"arguments[0].innerText = '* {overrides['reg']}'", element)
reg = overrides['reg']
else:
try:
reg = browser.find_element_by_id("selected_registration").get_attribute("innerHTML")
try:
reg = browser.find_element(By.ID, "selected_registration").get_attribute("innerHTML")
print("Reg from tar1090 is", reg)
except Exception as e:
print("Couldn't find reg in tar1090", e)
reg = None
reg = None
if reg is not None:
try:
try:
photo_box = browser.find_element_by_id("silhouette")
photo_box = browser.find_element(By.ID, "silhouette")
except NoSuchElementException:
photo_box = browser.find_element_by_id("airplanePhoto")
photo_box = browser.find_element(By.ID, "airplanePhoto")
finally:
import requests, json
photo_list = json.loads(requests.get("https://raw.githubusercontent.com/Jxck-S/aircraft-photos/main/photo-list.json", timeout=20).text)
@ -87,7 +101,7 @@ def get_adsbx_screenshot(file_path, url_params, enable_labels=False, enable_trac
browser.execute_script("arguments[0].style.width = '200px';", photo_box)
browser.execute_script("arguments[0].style.float = 'left';", photo_box)
browser.execute_script(f"arguments[0].src = 'https://raw.githubusercontent.com/Jxck-S/aircraft-photos/main/images/{reg}.jpg';", photo_box)
image_copy_right = browser.find_element_by_id("copyrightInfo")
image_copy_right = browser.find_element(By.ID, "copyrightInfo")
browser.execute_cdp_cmd('Emulation.setScriptExecutionDisabled', {'value': True})
copy_right_children = image_copy_right.find_elements(By.XPATH, "*")
if len(copy_right_children) > 0:
@ -97,16 +111,17 @@ def get_adsbx_screenshot(file_path, url_params, enable_labels=False, enable_trac
except Exception as e:
print("Error on changing photo", e)
if 'type' in overrides.keys():
element = browser.find_element_by_id("selected_icaotype")
element = browser.find_element(By.ID, "selected_icaotype")
browser.execute_script(f"arguments[0].innerText = '* {overrides['type']}'", element)
if 'typelong' in overrides.keys():
element = browser.find_element_by_id("selected_typelong")
element = browser.find_element(By.ID, "selected_typelong")
browser.execute_script(f"arguments[0].innerText = '* {overrides['typelong']}'", element)
if 'ownop' in overrides.keys():
element = browser.find_element_by_id("selected_ownop")
element = browser.find_element(By.ID, "selected_ownop")
browser.execute_script(f"arguments[0].innerText = '* {overrides['ownop']}'", element)
time.sleep(5)
browser.save_screenshot(file_path)
browser.quit()
def generate_adsbx_screenshot_time_params(timestamp):
from datetime import datetime
from datetime import timedelta

56
defTelegram.py

@ -1,4 +1,17 @@
def sendTeleg(photo, message, config):
try:
from telegram import __version_info__
except ImportError:
__version_info__ = (0, 0, 0, 0, 0)
if __version_info__ < (20, 0, 0, "alpha", 5):
sent = sendTelegOld(photo, message, config)
return sent
else:
import asyncio
sent = asyncio.run(t_send_photo(photo,message,config))
return sent
def sendTelegOld(photo, message, config):
import telegram
sent = False
retry_c = 0
@ -6,6 +19,45 @@ def sendTeleg(photo, message, config):
try:
bot = telegram.Bot(token=config.get('TELEGRAM', 'BOT_TOKEN'), request=telegram.utils.request.Request(connect_timeout=20, read_timeout=20))
sent = bot.send_photo(chat_id=config.get('TELEGRAM', 'ROOM_ID'), photo=photo, caption=message, parse_mode=telegram.ParseMode.MARKDOWN, timeout=20)
except Exception as err:
print('err.args:')
print(err.args)
print(f"Unexpected {err=}, {type(err)=}")
print("\nString err:\n"+str(err))
if retry_c > 4:
print('Telegram attempts exceeded. Message not sent.')
break
elif str(err) == 'Unauthorized':
print('Invalid Telegram bot token, message not sent.')
break
elif str(err) == 'Timed out':
retry_c += 1
print('Telegram timeout count: '+str(retry_c))
pass
elif str(err) == 'Chat not found':
print('Invalid Telegram Chat ID, message not sent.')
break
elif str(err)[:35] == '[Errno 2] No such file or directory':
print('Telegram module couldn\'t find an image to sent.')
break
elif str(err) == 'Media_caption_too_long':
print('Telegram image caption length exceeds 1024 characters. Message not sent.')
break
else:
print('[X] Unknown Telegram error. Message not sent.')
break
else:
print("Telegram message successfully sent.")
return sent
async def t_send_photo(photo,message,config):
import telegram
sent = False
retry_c = 0
while sent == False:
try:
bot = telegram.Bot(token=config.get('TELEGRAM', 'BOT_TOKEN'))
sent = await bot.send_photo(chat_id=config.get('TELEGRAM', 'ROOM_ID'), photo=photo, caption=message)
except Exception as err:
print('err.args:')
print(err.args)
@ -28,11 +80,11 @@ def sendTeleg(photo, message, config):
print('Telegram module couldn\'t find an image to send.')
break
elif str(err) == 'Media_caption_too_long':
print('Telegram image caption lenght exceeds 1024 characters. Message not send.')
print('Telegram image caption length exceeds 1024 characters. Message not sent.')
break
else:
print('[X] Unknown Telegram error. Message not sent.')
break
else:
print("Telegram message successfully sent.")
return sent
return sent

1
docker-compose.yml

@ -2,6 +2,7 @@ version: "3.9"
services:
plane-notify:
platform: linux/amd64
shm_size: 2gb
build:
context: .
volumes:

15
fuel_calc.py

@ -14,25 +14,24 @@ def get_avg_fuel_price():
except Exception as e:
print(e)
return None
def fuel_calculation(aircraft_icao_type, minutes):
"""Calculates fuel usage, price, c02 output of a flight depending on aircraft type and flight length"""
with open("aircraft_type_fuel_consumption_rates.json", "r") as f:
fuellist = json.loads(f.read())
#avg_fuel_price_per_gallon = 5.08
fuel_flight_info = {}
if aircraft_icao_type in fuellist.keys():
avg_fuel_price_per_gallon = get_avg_fuel_price()
galph = fuellist[aircraft_icao_type]["galph"]
fuel_used_gal = round(galph * (minutes/60), 2)
fuel_used_gal = galph * (minutes/60)
fuel_flight_info["fuel_price"] = round(fuel_used_gal * avg_fuel_price_per_gallon)
fuel_used_kg = fuel_used_gal * 3.04
c02_tons = round((fuel_used_kg * 3.15 ) / 907.185)
c02_tons = (fuel_used_kg * 3.15 ) / 907.185
fuel_flight_info['fuel_used_kg'] = round(fuel_used_kg)
fuel_flight_info["fuel_used_gal"] = round(fuel_used_gal)
fuel_flight_info['fuel_used_lters'] = round(fuel_used_gal*3.78541)
fuel_flight_info["fuel_used_lbs"] = round(fuel_used_kg * 2.20462)
fuel_flight_info["c02_tons"] = c02_tons
fuel_flight_info["c02_tons"] = round(c02_tons) if c02_tons > 1 else round(c02_tons, 4)
print ("Fuel info", fuel_flight_info)
return fuel_flight_info
else:
@ -43,10 +42,8 @@ def fuel_message(fuel_info):
cost = "{:,}".format(fuel_info['fuel_price'])
gallons = "{:,}".format(fuel_info['fuel_used_gal'])
lters = "{:,}".format(fuel_info['fuel_used_lters'])
lbs = "{:, }".format(fuel_info['fuel_used_lbs'])
lbs = "{:,}".format(fuel_info['fuel_used_lbs'])
kgs = "{:,}".format(fuel_info['fuel_used_kg'])
fuel_message = f"~ {gallons} gallons ({lters} liters). \n~ {lbs} lbs ({kgs} kg) of jet fuel used. \n~ ${cost} cost of fuel. \n~ {fuel_info['c02_tons']} tons of CO2 emissions."
fuel_message = f"\n~ {gallons} gallons ({lters} liters). \n~ {lbs} lbs ({kgs} kg) of jet fuel used. \n~ ${cost} cost of fuel. \n~ {fuel_info['c02_tons']} tons of CO2 emissions."
print(fuel_message)
return fuel_message
#fuel_info = fuel_calculation("GLF6", 548.1)
#fuel_message(fuel_info)

5
modify_image.py

@ -26,11 +26,6 @@ def append_airport(filename, airport, text_credit=None):
draw.rectangle(((325, 760), (624, 800)), fill= white, outline=black)
#Header Box
draw.rectangle(((401, 738), (549, 760)), fill= navish)
#ADSBX Logo
#
# adsbx = Image.open("./dependencies/ADSBX_Logo.png")
# adsbx = adsbx.resize((25, 25), Image.ANTIALIAS)
# image.paste(adsbx, (632, 757), adsbx)
#Create Text
#ADSBX Credit
if text_credit is not None:

130
planeClass.py

@ -8,6 +8,7 @@ class Plane:
self.icao = icao.upper()
self.callsign = None
self.config = config
self.config_path = config_path
self.overrides = {}
if self.config.has_option('DATA', 'OVERRIDE_REG'):
self.reg = self.config.get('DATA', 'OVERRIDE_REG')
@ -23,6 +24,14 @@ class Plane:
self.overrides['typelong'] = self.config.get('DATA', 'OVERRIDE_TYPELONG')
if self.config.has_option('DATA', 'OVERRIDE_OWNER'):
self.overrides['ownop'] = self.config.get('DATA', 'OVERRIDE_OWNER')
if self.config.has_option('DATA', 'CONCEAL_AC_ID'):
self.conceal_ac_id = self.config.getboolean('DATA', 'CONCEAL_AC_ID')
else:
self.conceal_ac_id = False
if self.config.has_option('DATA', 'CONCEAL_PIA'):
self.conceal_pia = self.config.getboolean('DATA', 'CONCEAL_PIA')
else:
self.conceal_pia = False
self.conf_file_path = config_path
self.alt_ft = None
self.below_desired_ft = None
@ -55,6 +64,7 @@ class Plane:
self.track = None
self.last_track = None
self.circle_history = None
self.nearest_from_airport = None
if self.config.has_option('DATA', 'DATA_LOSS_MINS'):
self.data_loss_mins = self.config.getint('DATA', 'DATA_LOSS_MINS')
else:
@ -69,18 +79,20 @@ class Plane:
self.latest_tweet_id = self.tweet_api.user_timeline(count = 1)[0]
except IndexError:
self.latest_tweet_id = None
#Setup PushBullet
if self.config.getboolean('PUSHBULLET', 'ENABLE'):
from pushbullet import Pushbullet
self.pb = Pushbullet(self.config['PUSHBULLET']['API_KEY'])
self.pb_channel = self.pb.get_channel(self.config.get('PUSHBULLET', 'CHANNEL_TAG'))
def run_opens(self, ac_dict):
#Parse OpenSky Vector
from colorama import Fore, Back, Style
self.print_header("BEGIN")
#print (Fore.YELLOW + "OpenSky Sourced Data: ", ac_dict)
try:
self.__dict__.update({'icao' : ac_dict.icao24.upper(), 'callsign' : ac_dict.callsign, 'latitude' : ac_dict.latitude, 'longitude' : ac_dict.longitude, 'on_ground' : bool(ac_dict.on_ground), 'squawk' : ac_dict.squawk, 'track' : float(ac_dict.heading)})
self.__dict__.update({
'icao' : ac_dict.icao24.upper(),
'callsign' : ac_dict.callsign,
'latitude' : ac_dict.latitude,
'longitude' : ac_dict.longitude,
'on_ground' : bool(ac_dict.on_ground),
'squawk' : ac_dict.squawk,
'track' : float(ac_dict.true_track)})
if ac_dict.baro_altitude != None:
self.alt_ft = round(float(ac_dict.baro_altitude) * 3.281)
elif self.on_ground:
@ -88,7 +100,8 @@ class Plane:
from mictronics_parse import get_aircraft_reg_by_icao, get_type_code_by_icao
self.reg = get_aircraft_reg_by_icao(self.icao)
self.type = get_type_code_by_icao(self.icao)
self.last_pos_datetime = datetime.fromtimestamp(ac_dict.time_position)
if ac_dict.time_position is not None:
self.last_pos_datetime = datetime.fromtimestamp(ac_dict.time_position)
except ValueError as e:
print("Got data but some data is invalid!")
print(e)
@ -221,8 +234,11 @@ class Plane:
def route_format(extra_route_info, type):
from defAirport import get_airport_by_icao
to_airport = get_airport_by_icao(self.known_to_airport)
code = to_airport['iata_code'] if to_airport['iata_code'] != "" else to_airport['icao']
airport_text = f"{code}, {to_airport['name']}"
if to_airport:
code = to_airport['iata_code'] if to_airport['iata_code'] != "" else to_airport['icao']
airport_text = f"{code}, {to_airport['name']}"
else:
airport_text = f"{self.known_to_airport}"
if 'time_to' in extra_route_info.keys() and type != "divert":
arrival_rel = "in ~" + extra_route_info['time_to']
else:
@ -234,7 +250,10 @@ class Plane:
header = "Now going to"
elif type == "divert":
header = "Now diverting to"
area = f"{to_airport['municipality']}, {to_airport['region']}, {to_airport['iso_country']}"
if to_airport:
area = f"{to_airport['municipality']}, {to_airport['region']}, {to_airport['iso_country']}"
else:
area = ""
route_to = f"{header} {area} ({airport_text})" + (f" arriving {arrival_rel}" if arrival_rel is not None else "")
else:
if type == "inital":
@ -388,7 +407,7 @@ class Plane:
else:
self.dis_title = self.config.get('DISCORD', 'TITLE')
#Set Twitter Title
if self.config.getboolean('TWITTER', 'ENABLE'):
if self.config.getboolean('TWITTER', 'ENABLE') and Plane.main_config.getboolean('TWITTER', 'ENABLE'):
if self.config.get('TWITTER', 'TITLE') in ["DYNAMIC", "callsign"]:
self.twitter_title = dynamic_title
else:
@ -423,12 +442,8 @@ class Plane:
elif self.landed:
landed_time_msg = None
landed_time = None
if self.icao != "A835AF":
message = (f"{type_header} {location_string}.") + ("" if route_to is None else f" {route_to}.") + ((f" {landed_time_msg}") if landed_time_msg != None else "")
dirty_message = None
else:
message = (f"{type_header} {location_string}.") + ((f" {landed_time_msg}") if landed_time_msg != None else "")
dirty_message = (f"{type_header} {location_string}.") + ("" if route_to is None else f" {route_to}.") + ((f" {landed_time_msg}") if landed_time_msg != None else "")
message = (f"{type_header} {location_string}.") + ("" if route_to is None else f" {route_to}.") + ((f" {landed_time_msg}") if landed_time_msg != None else "")
print (message)
#Google Map or tar1090 screenshot
if Plane.main_config.get('MAP', 'OPTION') == "GOOGLESTATICMAP":
@ -436,8 +451,8 @@ class Plane:
getMap((municipality + ", " + state + ", " + country_code), self.map_file_name)
elif Plane.main_config.get('MAP', 'OPTION') == "ADSBX":
from defSS import get_adsbx_screenshot
url_params = f"largeMode=2&hideButtons&hideSidebar&mapDim=0&zoom=10&icao={self.icao}&overlays={self.get_adsbx_map_overlays()}"
get_adsbx_screenshot(self.map_file_name, url_params, overrides=self.overrides)
url_params = f"largeMode=2&hideButtons&hideSidebar&mapDim=0&zoom=10&icao={self.icao}&overlays={self.get_adsbx_map_overlays()}&limitupdates=0"
get_adsbx_screenshot(self.map_file_name, url_params, overrides=self.overrides, conceal_ac_id=self.conceal_ac_id, conceal_pia=self.conceal_pia)
from modify_image import append_airport
text_credit = self.config.get('MAP', 'TEXT_CREDIT') if self.config.has_option('MAP', 'TEXT_CREDIT') else None
append_airport(self.map_file_name, nearest_airport_dict, text_credit)
@ -448,19 +463,17 @@ class Plane:
from defTelegram import sendTeleg
photo = open(self.map_file_name, "rb")
sendTeleg(photo, message, self.config)
#Mastodon
if self.config.has_section('MASTODON') and self.config.getboolean('MASTODON', 'ENABLE'):
from defMastodon import sendMastodon
sendMastodon(self.map_file_name, message, self.config)
#Discord
if self.config.getboolean('DISCORD', 'ENABLE'):
dis_message = f"{self.dis_title} {message}".strip() if dirty_message is None else f"{self.dis_title} {dirty_message}".strip()
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None
sendDis(dis_message, self.config, role_id, self.map_file_name)
#PushBullet
if self.config.getboolean('PUSHBULLET', 'ENABLE'):
with open(self.map_file_name, "rb") as pic:
map_data = self.pb.upload_file(pic, "Tookoff IMG" if self.tookoff else "Landed IMG")
self.pb_channel.push_note(self.config.get('PUSHBULLET', 'TITLE'), message)
self.pb_channel.push_file(**map_data)
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') and self.config.get('DISCORD', 'ROLE_ID').strip() != "" else None
sendDis(message, self.config, role_id, self.map_file_name)
#Twitter
if self.config.getboolean('TWITTER', 'ENABLE'):
if self.config.getboolean('TWITTER', 'ENABLE') and Plane.main_config.getboolean('TWITTER', 'ENABLE'):
import tweepy
try:
twitter_media_map_obj = self.tweet_api.media_upload(self.map_file_name)
@ -468,21 +481,20 @@ class Plane:
self.tweet_api.create_media_metadata(media_id= twitter_media_map_obj.media_id, alt_text= alt_text)
self.latest_tweet_id = self.tweet_api.update_status(status = ((self.twitter_title + " " + message).strip()), media_ids=[twitter_media_map_obj.media_id]).id
except tweepy.errors.TweepyException as e:
print(e)
raise Exception(self.icao) from e
raise
#Meta
if self.config.has_option('META', 'ENABLE') and self.config.getboolean('META', 'ENABLE'):
from meta_toolkit import post_to_meta_both
post_to_meta_both(self.config.get("META", "FB_PAGE_ID"), self.config.get("META", "IG_USER_ID"), self.map_file_name, message, self.config.get("META", "ACCESS_TOKEN"))
os.remove(self.map_file_name)
if self.landed:
if self.known_to_airport is not None and self.nearest_from_airport is not None and self.known_to_airport != self.nearest_from_airport:
if nearest_airport_dict is not None and self.nearest_from_airport is not None and nearest_airport_dict['icao'] != self.nearest_from_airport:
from defAirport import get_airport_by_icao
from geopy.distance import geodesic
known_to_airport = get_airport_by_icao(self.known_to_airport)
landed_airport = nearest_airport_dict
nearest_from_airport = get_airport_by_icao(self.nearest_from_airport)
from_coord = (nearest_from_airport['latitude_deg'], nearest_from_airport['longitude_deg'])
to_coord = (known_to_airport['latitude_deg'], known_to_airport['longitude_deg'])
to_coord = (landed_airport['latitude_deg'], landed_airport['longitude_deg'])
distance_mi = float(geodesic(from_coord, to_coord).mi)
distance_nm = distance_mi / 1.150779448
distance_message = f"{'{:,}'.format(round(distance_mi))} mile ({'{:,}'.format(round(distance_nm))} NM) flight from {nearest_from_airport['iata_code'] if nearest_from_airport['iata_code'] != '' else nearest_from_airport['ident']} to {nearest_airport_dict['iata_code'] if nearest_airport_dict['iata_code'] != '' else nearest_airport_dict['ident']}\n"
@ -497,14 +509,13 @@ class Plane:
fuel_message = fuel_message(fuel_info)
if self.config.getboolean('DISCORD', 'ENABLE'):
dis_message = f"{self.dis_title} {distance_message} \nFlight Fuel Info ```{fuel_message}```".strip()
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') and self.config.get('DISCORD', 'ROLE_ID').strip() != "" else None
sendDis(dis_message, self.config, role_id)
if self.config.getboolean('TWITTER', 'ENABLE'):
if self.config.getboolean('TWITTER', 'ENABLE') and Plane.main_config.getboolean('TWITTER', 'ENABLE'):
try:
self.latest_tweet_id = self.tweet_api.update_status(status = ((self.twitter_title + " " + distance_message + " " + fuel_message).strip()), in_reply_to_status_id = self.latest_tweet_id).id
except tweepy.errors.TweepyException as e:
print(e)
raise Exception(self.icao) from e
raise
self.latest_tweet_id = None
self.recheck_route_time = None
self.known_to_airport = None
@ -524,11 +535,10 @@ class Plane:
#Discord
if self.config.getboolean('DISCORD', 'ENABLE'):
dis_message = f"{self.dis_title} {route_to}".strip()
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') and self.config.get('DISCORD', 'ROLE_ID').strip() != "" else None
sendDis(dis_message, self.config, role_id)
#Twitter
if self.config.getboolean('TWITTER', 'ENABLE') and self.icao == 'A835AF':
#tweet = self.tweet_api.user_timeline(count = 1)[0]
if self.config.getboolean('TWITTER', 'ENABLE') and Plane.main_config.getboolean('TWITTER', 'ENABLE'):
self.latest_tweet_id = self.tweet_api.update_status(status = f"{self.twitter_title} {route_to}".strip(), in_reply_to_status_id = self.latest_tweet_id).id
if self.circle_history is not None:
@ -558,8 +568,8 @@ class Plane:
getMap((municipality + ", " + state + ", " + country_code), self.map_file_name)
if Plane.main_config.get('MAP', 'OPTION') == "ADSBX":
from defSS import get_adsbx_screenshot
url_params = f"largeMode=2&hideButtons&hideSidebar&mapDim=0&zoom=10&icao={self.icao}&overlays={self.get_adsbx_map_overlays()}"
get_adsbx_screenshot(self.map_file_name, url_params, overrides=self.overrides)
url_params = f"largeMode=2&hideButtons&hideSidebar&mapDim=0&zoom=10&icao={self.icao}&overlays={self.get_adsbx_map_overlays()}&limitupdates=0"
get_adsbx_screenshot(self.map_file_name, url_params, overrides=self.overrides, conceal_ac_id=self.conceal_ac_id, conceal_pia=self.conceal_pia)
if self.config.getboolean('DISCORD', 'ENABLE'):
dis_message = (self.dis_title + " " + squawk_message)
sendDis(dis_message, self.config, None, self.map_file_name)
@ -581,8 +591,8 @@ class Plane:
dis_message = (self.dis_title + " " + mode + " mode enabled.")
if mode == "Approach":
from defSS import get_adsbx_screenshot
url_params = f"largeMode=2&hideButtons&hideSidebar&mapDim=0&zoom=10&icao={self.icao}&overlays={self.get_adsbx_map_overlays()}"
get_adsbx_screenshot(self.map_file_name, url_params, overrides=self.overrides)
url_params = f"largeMode=2&hideButtons&hideSidebar&mapDim=0&zoom=10&icao={self.icao}&overlays={self.get_adsbx_map_overlays()}&limitupdates=0"
get_adsbx_screenshot(self.map_file_name, url_params, overrides=self.overrides, conceal_ac_id=self.conceal_ac_id, conceal_pia=self.conceal_pia)
sendDis(dis_message, self.config, None, self.map_file_name)
#elif mode in ["Althold", "VNAV", "LNAV"] and self.sel_nav_alt != None:
# sendDis((dis_message + ", Sel Alt. " + str(self.sel_nav_alt) + ", Current Alt. " + str(self.alt_ft)), self.config)
@ -608,7 +618,8 @@ class Plane:
from calculate_headings import calculate_deg_change
track_change = calculate_deg_change(self.track, self.last_track)
track_change = round(track_change, 3)
self.circle_history["traces"].append((time.time(), self.latitude, self.longitude, track_change))
if self.latitude is not None and self.longitude is not None:
self.circle_history["traces"].append((time.time(), self.latitude, self.longitude, track_change))
total_change = 0
coords = []
@ -648,7 +659,7 @@ class Plane:
in_tfr = None
if Plane.main_config.getboolean("TFRS", "ENABLE"):
tfr_url = Plane.main_config.get("TFRS", "URL")
response = requests.get(tfr_url, timeout=30)
response = requests.get(tfr_url, timeout=60)
tfrs = json.loads(response.text)
for tfr in tfrs:
if in_tfr is not None:
@ -764,9 +775,8 @@ class Plane:
return tfr_map_filename
from defSS import get_adsbx_screenshot
url_params = f"largeMode=2&hideButtons&hideSidebar&mapDim=0&zoom=10&icao={self.icao}&overlays={self.get_adsbx_map_overlays()}"
get_adsbx_screenshot(self.map_file_name, url_params, overrides=self.overrides)
url_params = f"largeMode=2&hideButtons&hideSidebar&mapDim=0&zoom=10&icao={self.icao}&overlays={self.get_adsbx_map_overlays()}&limitupdates=0"
get_adsbx_screenshot(self.map_file_name, url_params, overrides=self.overrides, conceal_ac_id=self.conceal_ac_id, conceal_pia=self.conceal_pia)
if nearest_airport_dict['distance_mi'] < 3:
if "touchngo" in self.circle_history.keys():
message = f"Doing touch and goes at {nearest_airport_dict['icao']}"
@ -776,8 +786,8 @@ class Plane:
message = f"Circling {round(nearest_airport_dict['distance_mi'], 2)}mi {cardinal} of {nearest_airport_dict['icao']}, {nearest_airport_dict['name']} at {self.alt_ft}ft. "
tfr_map_filename = None
if in_tfr is not None:
context = "Inside" if 'context' not in in_tfr.keys() else "Above" if in_tfr['context'] == 'above' else "Below"
message += f" {context} TFR {in_tfr['info']['NOTAM']}, a TFR for {in_tfr['info']['Type'].title()}"
wording_context = "Inside" if 'context' not in in_tfr.keys() else "Above" if in_tfr['context'] == 'above' else "Below"
message += f" {wording_context} TFR {in_tfr['info']['NOTAM']}, a TFR for {in_tfr['info']['Type'].title()}"
tfr_map_filename = tfr_image(context, (self.latitude, self.longitude))
elif in_tfr is None and closest_tfr is not None and "distance" in closest_tfr.keys() and closest_tfr["distance"] <= 20:
message += f" {closest_tfr['distance']} miles from TFR {closest_tfr['info']['NOTAM']}, a TFR for {closest_tfr['info']['Type']}"
@ -793,12 +803,12 @@ class Plane:
from defTelegram import sendTeleg
sendTeleg(photo, message, self.config)
if self.config.getboolean('DISCORD', 'ENABLE'):
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None
if tfr_map_filename is not None:
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') and self.config.get('DISCORD', 'ROLE_ID').strip() != "" else None
if tfr_map_filename is not None:
sendDis(message, self.config, role_id, self.map_file_name, tfr_map_filename)
elif tfr_map_filename is None:
sendDis(message, self.config, role_id, self.map_file_name)
if self.config.getboolean('TWITTER', 'ENABLE'):
if self.config.getboolean('TWITTER', 'ENABLE') and Plane.main_config.getboolean('TWITTER', 'ENABLE'):
twitter_media_map_obj = self.tweet_api.media_upload(self.map_file_name)
media_ids = [twitter_media_map_obj.media_id]
if tfr_map_filename is not None:
@ -812,6 +822,10 @@ class Plane:
if self.config.has_option('META', 'ENABLE') and self.config.getboolean('META', 'ENABLE'):
from meta_toolkit import post_to_meta_both
post_to_meta_both(self.config.get("META", "FB_PAGE_ID"), self.config.get("META", "IG_USER_ID"), self.map_file_name, message, self.config.get("META", "ACCESS_TOKEN"))
#Mastodon
if self.config.has_section('MASTODON') and self.config.getboolean('MASTODON', 'ENABLE'):
from defMastodon import sendMastodon
sendMastodon(self.map_file_name, message, self.config)
self.circle_history['triggered'] = True
elif abs(total_change) <= 360 and self.circle_history["triggered"]:
print("No Longer Circling, trigger cleared")
@ -850,7 +864,7 @@ class Plane:
if bool(int(ra['acas_ra']['MTE'])):
ra_message += ", Multi threat"
from defSS import get_adsbx_screenshot, generate_adsbx_screenshot_time_params
url_params = f"&lat={ra['lat']}&lon={ra['lon']}&zoom=11&largeMode=2&hideButtons&hideSidebar&mapDim=0&overlays={self.get_adsbx_map_overlays()}"
url_params = f"&lat={ra['lat']}&lon={ra['lon']}&zoom=11&largeMode=2&hideButtons&hideSidebar&mapDim=0&overlays={self.get_adsbx_map_overlays()}&limitupdates=0"
if "threat_id_hex" in ra['acas_ra'].keys():
from mictronics_parse import get_aircraft_reg_by_icao
threat_reg = get_aircraft_reg_by_icao(ra['acas_ra']['threat_id_hex'])
@ -860,12 +874,12 @@ class Plane:
else:
url_params += f"&icao={self.icao.lower()}&noIsolation"
print(url_params)
get_adsbx_screenshot(self.map_file_name, url_params, True, True, overrides=self.overrides)
get_adsbx_screenshot(self.map_file_name, url_params, True, True, overrides=self.overrides, conceal_ac_id=self.conceal_ac_id, conceal_pia=self.conceal_pia)
if self.config.getboolean('DISCORD', 'ENABLE'):
from defDiscord import sendDis
dis_message = f"{self.dis_title} {ra_message}"
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None
role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') and self.config.get('DISCORD', 'ROLE_ID').strip() != "" else None
sendDis(dis_message, self.config, role_id, self.map_file_name)
#if twitter
def expire_ra_types(self):

22
requirements.txt

@ -0,0 +1,22 @@
# requirements.txt
colorama
geopy
tabulate
pytz
pillow
tweepy
discord-webhook
selenium
git+https://github.com/openskynetwork/opensky-api.git@master#subdirectory=python/
webdriver-manager
shapely
pandas
python-telegram-bot
mastodon.py
beautifulsoup4
pycairo
py-staticmaps
pyproj
lxml
configparser
geog
Loading…
Cancel
Save