parent
b9ed0bf0d9
commit
d5570bcec7
63
README.md
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 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.
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
@ -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
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
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…
Reference in New Issue
Block a user