Browse Source

Add geofencing and API support

Closes #8
main
Samuel Sloniker 11 months ago
parent
commit
d5570bcec7
  1. 63
      README.md
  2. 39
      adsms/__init__.py
  3. 34
      adsms/db.py
  4. 10
      convert.py

63
README.md

@ -12,38 +12,67 @@ Copy the configuration file, make any necessary changes, and run:
## Configuration file ## Configuration file
* `textbelt_key`: your [Textbelt](https://textbelt.com) API key All values are required. All are strings unless otherwise specified. All times
* `data`: a URL to a readsb/tar1090 `aircraft.json` endpoint 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/) * `tracker`: a URL to a tar1090 tracker (e.g. https://globe.theairtraffic.com/)
* `database`: an SQLite file in which to store subscriptions * `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 * `pid_file`: path to which to write the PID (set to empty string to not write
a PID file) a PID file)
* `max_age`: maximum age of aircraft pings in seconds; pings older than this * `max_age`: maximum age of aircraft pings; pings older than this will be
will be ignored ignored
* `min_disappearance`: the minimum time in seconds for which an aircraft must * `min_disappearance`: the minimum time for which an aircraft must go "off the
go "off the radar" before disappearing for new pings to trigger notifications radar" before disappearing for new pings to trigger notifications again
again
* `delay`: time to wait after processing all rules before running the loop * `delay`: time to wait after processing all rules before running the loop
again again
## Database Schema ## 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` ### `subscriptions`
The `subscriptions` table has the following columns: The `subscriptions` table has the following columns:
| Column Name | Data Type | Description | | Column Name | Data Type | Description |
| ------------- | --------- | -------------------------------------------------------------- | | ------------- | --------- | ---------------------------------------------------------------- |
| `rowid` | INTEGER | Unique identifier for the subscription. | | `rowid` | INTEGER | Unique identifier for the subscription. |
| `phone` | TEXT | Phone number to receive notifications for this subscription. | | `phone` | TEXT | Identifier to receive notifications for this subscription. |
| `icao` | TEXT | ICAO address of the aircraft to track. | | `icao` | TEXT | ICAO address of the aircraft to track. |
| `description` | TEXT | Description of the aircraft being tracked. | | `description` | TEXT | Description of the aircraft being tracked. |
| `last_seen` | INTEGER | Timestamp of the last time this aircraft was seen by the system.| | `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 ## Use of ChatGPT
Portions of both this README and the `adsms` code have been partially written with ChatGPT. Portions of both this README and the `adsms` code have been partially written with ChatGPT.

39
adsms/__init__.py

@ -22,9 +22,25 @@ def send_text_message(phone, message, key):
def process_subscriptions(con, config, data): def process_subscriptions(con, config, data):
subscriptions = db.get_subscriptions(con) subscriptions = db.get_subscriptions(con)
print(subscriptions)
for sub_id, phone, icao, description, last_seen, platform in subscriptions: for (
if icao in data and data[icao]["seen"] < config["max_age"]: 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(): if last_seen + config["min_disappearance"] < time.time():
message = f"{description}\n{config['tracker']}?icao={icao}" message = f"{description}\n{config['tracker']}?icao={icao}"
@ -44,18 +60,27 @@ def process_subscriptions(con, config, data):
con.commit() con.commit()
def get_current_data(config): def get_current_data(con, config):
# return {"78007e": {"seen": 0}} # return {"78007e": {"seen": 0}}
response = requests.get(config["data"]) print(
planes = response.json()["aircraft"] 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} return {plane["hex"]: plane for plane in planes}
def run(config): def run(config):
con = adsms.db.load_database(config["database"]) con = adsms.db.load_database(config["database"])
print(db.get_all_icao(con))
while True: while True:
data = get_current_data(config) data = get_current_data(con, config)
process_subscriptions(con, config, data) process_subscriptions(con, config, data)

34
adsms/db.py

@ -11,6 +11,10 @@ Subscription = collections.namedtuple(
"description", "description",
"last_seen", "last_seen",
"platform", "platform",
"min_lat",
"min_lon",
"max_lat",
"max_lon",
], ],
) )
@ -18,16 +22,21 @@ Subscription = collections.namedtuple(
def load_database(file_name): def load_database(file_name):
con = sqlite3.connect(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)" "CREATE TABLE IF NOT EXISTS subscriptions(phone VARCHAR, icao VARCHAR, description VARCHAR, last_seen INTEGER)"
) )
try: for query in [
cur.execute( "ALTER TABLE subscriptions ADD COLUMN platform VARCHAR DEFAULT 'textbelt'",
"ALTER TABLE subscriptions ADD COLUMN platform DEFAULT 'textbelt'" "ALTER TABLE subscriptions ADD COLUMN min_lat REAL DEFAULT -90",
) "ALTER TABLE subscriptions ADD COLUMN min_lon REAL DEFAULT -180",
except sqlite3.OperationalError: "ALTER TABLE subscriptions ADD COLUMN max_lat REAL DEFAULT 90",
pass "ALTER TABLE subscriptions ADD COLUMN max_lon REAL DEFAULT 180",
]:
try:
con.execute(query)
except sqlite3.OperationalError:
pass
con.commit() con.commit()
@ -43,6 +52,15 @@ def update_last_seen_time(con, sub_id):
def get_subscriptions(con): def get_subscriptions(con):
for subscription in con.execute( 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(): ).fetchall():
yield Subscription(*subscription) yield Subscription(*subscription)
def get_all_icao(con):
return [
i[0]
for i in con.execute(
"SELECT DISTINCT icao FROM subscriptions"
).fetchall()
]

10
convert.py

@ -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()
Loading…
Cancel
Save