From d5570bcec7c3b876b9b2bdaf67549e8eb37a09f4 Mon Sep 17 00:00:00 2001 From: Samuel Sloniker Date: Tue, 6 Jun 2023 18:38:31 -0700 Subject: [PATCH] Add geofencing and API support Closes #8 --- README.md | 63 ++++++++++++++++++++++++++++++++++------------- adsms/__init__.py | 39 +++++++++++++++++++++++------ adsms/db.py | 34 +++++++++++++++++++------ convert.py | 10 -------- 4 files changed, 104 insertions(+), 42 deletions(-) delete mode 100644 convert.py diff --git a/README.md b/README.md index 73e9d31..8289655 100644 --- a/README.md +++ b/README.md @@ -12,38 +12,67 @@ Copy the configuration file, make any necessary changes, and run: ## Configuration file -* `textbelt_key`: your [Textbelt](https://textbelt.com) API key -* `data`: a URL to a readsb/tar1090 `aircraft.json` endpoint +All values are required. All are strings unless otherwise specified. All times +are in seconds. + +* `textbelt_key`: your [Textbelt](https://textbelt.com) API key. This is always + required, but can be set to an invalid value if you do not plan to use SMS. +* `data`: a URL to a readsb/tar1090 `aircraft.json` endpoint (for a local + tracker), or an [adsb.one](https://adsb.one)-style `/hex/` API endpoint. For + adsb.one, this should be `https://api.adsb.one/v2/hex/`. The final slash is + important. +* `codes` (boolean): whether or not to append ICAO codes to the URL (`false` + for local tracker; `true` for adsb.one API) * `tracker`: a URL to a tar1090 tracker (e.g. https://globe.theairtraffic.com/) * `database`: an SQLite file in which to store subscriptions * `pid_file`: path to which to write the PID (set to empty string to not write a PID file) -* `max_age`: maximum age of aircraft pings in seconds; pings older than this - will be ignored -* `min_disappearance`: the minimum time in seconds for which an aircraft must - go "off the radar" before disappearing for new pings to trigger notifications - again +* `max_age`: maximum age of aircraft pings; pings older than this will be + ignored +* `min_disappearance`: the minimum time for which an aircraft must go "off the + radar" before disappearing for new pings to trigger notifications again * `delay`: time to wait after processing all rules before running the loop again ## Database Schema -`adsms` uses a SQLite database to store subscriptions and information about tracked aircraft. Currently, the only table is `subscriptions`. +`adsms` uses a SQLite database to store subscriptions and information about +tracked aircraft. Currently, the only table is `subscriptions`. ### `subscriptions` The `subscriptions` table has the following columns: -| Column Name | Data Type | Description | -| ------------- | --------- | -------------------------------------------------------------- | -| `rowid` | INTEGER | Unique identifier for the subscription. | -| `phone` | TEXT | Phone number to receive notifications for this subscription. | -| `icao` | TEXT | ICAO address of the aircraft to track. | -| `description` | TEXT | Description of the aircraft being tracked. | -| `last_seen` | INTEGER | Timestamp of the last time this aircraft was seen by the system.| +| Column Name | Data Type | Description | +| ------------- | --------- | ---------------------------------------------------------------- | +| `rowid` | INTEGER | Unique identifier for the subscription. | +| `phone` | TEXT | Identifier to receive notifications for this subscription. | +| `icao` | TEXT | ICAO address of the aircraft to track. | +| `description` | TEXT | Description of the aircraft being tracked. | +| `last_seen` | INTEGER | Timestamp of the last time this aircraft was seen by the system. | +| `platform` | TEXT | The method by which to send the message. | +| `min_lat` | REAL | The minimum latitude of the geofence. | +| `min_lon` | REAL | The minimum longitude of the geofence. | +| `max_lat` | REAL | The maximum latitude of the geofence. | +| `max_lon` | REAL | The maximum longitude of the geofence. | + +This table stores information about each subscription, including the contact +information to send notifications to, the ICAO address of the aircraft to +track, a description of the aircraft, and the last time it was seen by the +system. + +adsms can send messages by SMS using [Textbelt](https://textbelt.com) or by +Discord using webhooks. For SMS, use `textbelt` for `platform` and the phone +number for `phone`; for Discord, use `discord_webhook` for `platform` and the +webhook URL for `phone`. (The field is called `phone` because adsms originally +only supported SMS.) + +When adding new entries, set `last_seen` to 0. -This table stores information about each subscription, including the phone number to send notifications to, the ICAO address of the aircraft to track, a description of the aircraft, and the last time it was seen by the system. +If you want to notify whenever an aircraft is seen anywhere, use -90 for +`min_lat`, -180 for `min_lon`, 90 for `max_lat`, and 180 for `max_lon`. This +will cover the entire globe. ## Use of ChatGPT -Portions of both this README and the `adsms` code have been partially written with ChatGPT. \ No newline at end of file +Portions of both this README and the `adsms` code have been partially written with ChatGPT. diff --git a/adsms/__init__.py b/adsms/__init__.py index f05f02f..c323438 100755 --- a/adsms/__init__.py +++ b/adsms/__init__.py @@ -22,9 +22,25 @@ def send_text_message(phone, message, key): def process_subscriptions(con, config, data): subscriptions = db.get_subscriptions(con) - print(subscriptions) - for sub_id, phone, icao, description, last_seen, platform in subscriptions: - if icao in data and data[icao]["seen"] < config["max_age"]: + + for ( + sub_id, + phone, + icao, + description, + last_seen, + platform, + min_lat, + min_lon, + max_lat, + max_lon, + ) in subscriptions: + if ( + icao in data + and data[icao]["seen_pos"] < config["max_age"] + and min_lat <= data[icao]["lat"] <= max_lat + and min_lon <= data[icao]["lon"] <= max_lon + ): if last_seen + config["min_disappearance"] < time.time(): message = f"{description}\n{config['tracker']}?icao={icao}" @@ -44,18 +60,27 @@ def process_subscriptions(con, config, data): con.commit() -def get_current_data(config): +def get_current_data(con, config): # return {"78007e": {"seen": 0}} - response = requests.get(config["data"]) - planes = response.json()["aircraft"] + print( + config["data"] + + (",".join(db.get_all_icao(con)) if config["codes"] else "") + ) + response = requests.get( + config["data"] + + (",".join(db.get_all_icao(con)) if config["codes"] else "") + ).json() + planes = response.get("aircraft", response["ac"]) return {plane["hex"]: plane for plane in planes} def run(config): con = adsms.db.load_database(config["database"]) + print(db.get_all_icao(con)) + while True: - data = get_current_data(config) + data = get_current_data(con, config) process_subscriptions(con, config, data) diff --git a/adsms/db.py b/adsms/db.py index 1467712..f364388 100644 --- a/adsms/db.py +++ b/adsms/db.py @@ -11,6 +11,10 @@ Subscription = collections.namedtuple( "description", "last_seen", "platform", + "min_lat", + "min_lon", + "max_lat", + "max_lon", ], ) @@ -18,16 +22,21 @@ Subscription = collections.namedtuple( def load_database(file_name): con = sqlite3.connect(file_name) - cur = con.execute( + con.execute( "CREATE TABLE IF NOT EXISTS subscriptions(phone VARCHAR, icao VARCHAR, description VARCHAR, last_seen INTEGER)" ) - try: - cur.execute( - "ALTER TABLE subscriptions ADD COLUMN platform DEFAULT 'textbelt'" - ) - except sqlite3.OperationalError: - pass + for query in [ + "ALTER TABLE subscriptions ADD COLUMN platform VARCHAR DEFAULT 'textbelt'", + "ALTER TABLE subscriptions ADD COLUMN min_lat REAL DEFAULT -90", + "ALTER TABLE subscriptions ADD COLUMN min_lon REAL DEFAULT -180", + "ALTER TABLE subscriptions ADD COLUMN max_lat REAL DEFAULT 90", + "ALTER TABLE subscriptions ADD COLUMN max_lon REAL DEFAULT 180", + ]: + try: + con.execute(query) + except sqlite3.OperationalError: + pass con.commit() @@ -43,6 +52,15 @@ def update_last_seen_time(con, sub_id): def get_subscriptions(con): for subscription in con.execute( - "SELECT rowid, phone, icao, description, last_seen, platform FROM subscriptions" + "SELECT rowid, phone, icao, description, last_seen, platform, min_lat, min_lon, max_lat, max_lon FROM subscriptions" ).fetchall(): yield Subscription(*subscription) + + +def get_all_icao(con): + return [ + i[0] + for i in con.execute( + "SELECT DISTINCT icao FROM subscriptions" + ).fetchall() + ] diff --git a/convert.py b/convert.py deleted file mode 100644 index be30663..0000000 --- a/convert.py +++ /dev/null @@ -1,10 +0,0 @@ -import sqlite3 -import sys - -con = sqlite3.connect(sys.argv[1]) - -con.execute( - "ALTER TABLE subscriptions ADD COLUMN platform VARCHAR DEFAULT 'textbelt'" -) - -con.commit()