mirror of
https://github.com/craigerl/aprsd.git
synced 2024-12-18 23:55:58 -05:00
Add new watchlist feature
This patch adds a new optional feature called Watch list. Aprsd will filter IN all aprs packets from a list of callsigns. APRSD will keep track of the last time a callsign has been seen. When the configured timeout value has been reached, the next time a callsign is seen, APRSD will send the next packet from that callsign through the new notification plugins list. The new BaseNotifyPlugin is the default core APRSD notify based plugin. When it gets a packet it will construct a reply message to be sent to the configured alert callsign to alert them that the seen callsign is now on the APRS network. This basically acts as a notification that your watched callsign list is available on APRS. The new configuration options: aprsd: watch_list: # The callsign to send a message to once a watch list callsign # is now seen on APRS-IS alert_callsign: NOCALL # The time in seconds to wait for notification. # The default is 12 hours. alert_time_seconds: 43200 # The list of callsigns to watch for callsigns: - WB4BOR - KFART # Enable/disable this feature enabled: false # The list of notify based plugins to load for # processing a new seen packet from a callsign. enabled_plugins: - aprsd.plugins.notify.BaseNotifyPlugin This patch also adds a new section in the Admin UI for showing the watch list and the age of the last seen packet for each callsing since APRSD startup.
This commit is contained in:
parent
562ae52c1e
commit
1a1fcba1c4
@ -226,7 +226,7 @@ class Aprsdis(aprslib.IS):
|
|||||||
except ParseError as exp:
|
except ParseError as exp:
|
||||||
self.logger.log(11, "%s\n Packet: %s", exp.args[0], exp.args[1])
|
self.logger.log(11, "%s\n Packet: %s", exp.args[0], exp.args[1])
|
||||||
except UnknownFormat as exp:
|
except UnknownFormat as exp:
|
||||||
self.logger.log(9, "%s\n Packet: %s", exp.args[0], exp.args[1])
|
self.logger.log(9, "unknown format %s", exp.args)
|
||||||
except LoginError as exp:
|
except LoginError as exp:
|
||||||
self.logger.error("%s: %s", exp.__class__.__name__, exp.args[0])
|
self.logger.error("%s: %s", exp.__class__.__name__, exp.args[0])
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
@ -46,12 +46,30 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
@auth.login_required
|
@auth.login_required
|
||||||
def index(self):
|
def index(self):
|
||||||
stats = self._stats()
|
stats = self._stats()
|
||||||
|
LOG.debug(
|
||||||
|
"watch list? {}".format(
|
||||||
|
self.config["aprsd"]["watch_list"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if "watch_list" in self.config["aprsd"] and self.config["aprsd"][
|
||||||
|
"watch_list"
|
||||||
|
].get("enabled", False):
|
||||||
|
watch_count = len(self.config["aprsd"]["watch_list"]["callsigns"])
|
||||||
|
watch_age = self.config["aprsd"]["watch_list"]["alert_time_seconds"]
|
||||||
|
age_time = {"seconds": watch_age}
|
||||||
|
watch_age = datetime.timedelta(**age_time)
|
||||||
|
else:
|
||||||
|
watch_count = 0
|
||||||
|
watch_age = 0
|
||||||
|
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"index.html",
|
"index.html",
|
||||||
initial_stats=stats,
|
initial_stats=stats,
|
||||||
callsign=self.config["aprs"]["login"],
|
callsign=self.config["aprs"]["login"],
|
||||||
version=aprsd.__version__,
|
version=aprsd.__version__,
|
||||||
config_json=json.dumps(self.config),
|
config_json=json.dumps(self.config),
|
||||||
|
watch_count=watch_count,
|
||||||
|
watch_age=watch_age,
|
||||||
)
|
)
|
||||||
|
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
@ -92,6 +110,18 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
|
|
||||||
stats_dict = stats_obj.stats()
|
stats_dict = stats_obj.stats()
|
||||||
|
|
||||||
|
# Convert the watch_list entries to age
|
||||||
|
watch_list = stats_dict["aprsd"]["watch_list"]
|
||||||
|
new_list = {}
|
||||||
|
for call in watch_list:
|
||||||
|
call_date = datetime.datetime.strptime(
|
||||||
|
watch_list[call],
|
||||||
|
"%Y-%m-%d %H:%M:%S.%f",
|
||||||
|
)
|
||||||
|
new_list[call] = str(now - call_date)
|
||||||
|
|
||||||
|
stats_dict["aprsd"]["watch_list"] = new_list
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"time": now.strftime(time_format),
|
"time": now.strftime(time_format),
|
||||||
"size_tracker": len(track),
|
"size_tracker": len(track),
|
||||||
|
390
aprsd/listen.py
Normal file
390
aprsd/listen.py
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
#
|
||||||
|
# Listen on amateur radio aprs-is network for messages and respond to them.
|
||||||
|
# You must have an amateur radio callsign to use this software. You must
|
||||||
|
# create an ~/.aprsd/config.yml file with all of the required settings. To
|
||||||
|
# generate an example config.yml, just run aprsd, then copy the sample config
|
||||||
|
# to ~/.aprsd/config.yml and edit the settings.
|
||||||
|
#
|
||||||
|
# APRS messages:
|
||||||
|
# l(ocation) = descriptive location of calling station
|
||||||
|
# w(eather) = temp, (hi/low) forecast, later forecast
|
||||||
|
# t(ime) = respond with the current time
|
||||||
|
# f(ortune) = respond with a short fortune
|
||||||
|
# -email_addr email text = send an email
|
||||||
|
# -2 = display the last 2 emails received
|
||||||
|
# p(ing) = respond with Pong!/time
|
||||||
|
# anything else = respond with usage
|
||||||
|
#
|
||||||
|
# (C)2018 Craig Lamparter
|
||||||
|
# License GPLv2
|
||||||
|
#
|
||||||
|
|
||||||
|
# python included libs
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from logging import NullHandler
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
# local imports here
|
||||||
|
import aprsd
|
||||||
|
from aprsd import client, messaging, stats, threads, trace, utils
|
||||||
|
import aprslib
|
||||||
|
from aprslib.exceptions import LoginError
|
||||||
|
import click
|
||||||
|
import click_completion
|
||||||
|
|
||||||
|
# setup the global logger
|
||||||
|
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|
||||||
|
flask_enabled = False
|
||||||
|
|
||||||
|
# server_event = threading.Event()
|
||||||
|
|
||||||
|
# localization, please edit:
|
||||||
|
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
||||||
|
# USER = "KM6XXX-9" # callsign of this aprs client with SSID
|
||||||
|
# PASS = "99999" # google how to generate this
|
||||||
|
# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email
|
||||||
|
# shortcuts = {
|
||||||
|
# "aa" : "5551239999@vtext.com",
|
||||||
|
# "cl" : "craiglamparter@somedomain.org",
|
||||||
|
# "wb" : "5553909472@vtext.com"
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
|
def custom_startswith(string, incomplete):
|
||||||
|
"""A custom completion match that supports case insensitive matching."""
|
||||||
|
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||||
|
string = string.lower()
|
||||||
|
incomplete = incomplete.lower()
|
||||||
|
return string.startswith(incomplete)
|
||||||
|
|
||||||
|
|
||||||
|
click_completion.core.startswith = custom_startswith
|
||||||
|
click_completion.init()
|
||||||
|
|
||||||
|
|
||||||
|
cmd_help = """Shell completion for click-completion-command
|
||||||
|
Available shell types:
|
||||||
|
\b
|
||||||
|
%s
|
||||||
|
Default type: auto
|
||||||
|
""" % "\n ".join(
|
||||||
|
"{:<12} {}".format(k, click_completion.core.shells[k])
|
||||||
|
for k in sorted(click_completion.core.shells.keys())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||||
|
@click.version_option()
|
||||||
|
def main():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option(
|
||||||
|
"-i",
|
||||||
|
"--case-insensitive/--no-case-insensitive",
|
||||||
|
help="Case insensitive completion",
|
||||||
|
)
|
||||||
|
@click.argument(
|
||||||
|
"shell",
|
||||||
|
required=False,
|
||||||
|
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||||
|
)
|
||||||
|
def show(shell, case_insensitive):
|
||||||
|
"""Show the click-completion-command completion code"""
|
||||||
|
extra_env = (
|
||||||
|
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||||
|
if case_insensitive
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option(
|
||||||
|
"--append/--overwrite",
|
||||||
|
help="Append the completion code to the file",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"-i",
|
||||||
|
"--case-insensitive/--no-case-insensitive",
|
||||||
|
help="Case insensitive completion",
|
||||||
|
)
|
||||||
|
@click.argument(
|
||||||
|
"shell",
|
||||||
|
required=False,
|
||||||
|
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||||
|
)
|
||||||
|
@click.argument("path", required=False)
|
||||||
|
def install(append, case_insensitive, shell, path):
|
||||||
|
"""Install the click-completion-command completion"""
|
||||||
|
extra_env = (
|
||||||
|
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||||
|
if case_insensitive
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
shell, path = click_completion.core.install(
|
||||||
|
shell=shell,
|
||||||
|
path=path,
|
||||||
|
append=append,
|
||||||
|
extra_env=extra_env,
|
||||||
|
)
|
||||||
|
click.echo("{} completion installed in {}".format(shell, path))
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
global flask_enabled
|
||||||
|
|
||||||
|
threads.APRSDThreadList().stop_all()
|
||||||
|
if "subprocess" not in str(frame):
|
||||||
|
LOG.info(
|
||||||
|
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||||
|
datetime.datetime.now(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
tracker = messaging.MsgTrack()
|
||||||
|
tracker.save()
|
||||||
|
LOG.info(stats.APRSDStats())
|
||||||
|
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||||
|
# sys.exit(0)
|
||||||
|
if flask_enabled:
|
||||||
|
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||||
|
|
||||||
|
|
||||||
|
# Setup the logging faciility
|
||||||
|
# to disable logging to stdout, but still log to file
|
||||||
|
# use the --quiet option on the cmdln
|
||||||
|
def setup_logging(config, loglevel, quiet):
|
||||||
|
log_level = utils.LOG_LEVELS[loglevel]
|
||||||
|
LOG.setLevel(log_level)
|
||||||
|
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
||||||
|
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
||||||
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
|
log_file = config["aprsd"].get("logfile", None)
|
||||||
|
if log_file:
|
||||||
|
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||||
|
else:
|
||||||
|
fh = NullHandler()
|
||||||
|
|
||||||
|
fh.setFormatter(log_formatter)
|
||||||
|
LOG.addHandler(fh)
|
||||||
|
|
||||||
|
imap_logger = None
|
||||||
|
if config["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][
|
||||||
|
"imap"
|
||||||
|
].get("debug", False):
|
||||||
|
|
||||||
|
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||||
|
imap_logger.setLevel(log_level)
|
||||||
|
imap_logger.addHandler(fh)
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
sh = logging.StreamHandler(sys.stdout)
|
||||||
|
sh.setFormatter(log_formatter)
|
||||||
|
LOG.addHandler(sh)
|
||||||
|
if imap_logger:
|
||||||
|
imap_logger.addHandler(sh)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command()
|
||||||
|
@click.option(
|
||||||
|
"--loglevel",
|
||||||
|
default="DEBUG",
|
||||||
|
show_default=True,
|
||||||
|
type=click.Choice(
|
||||||
|
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||||
|
case_sensitive=False,
|
||||||
|
),
|
||||||
|
show_choices=True,
|
||||||
|
help="The log level to use for aprsd.log",
|
||||||
|
)
|
||||||
|
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
||||||
|
@click.option(
|
||||||
|
"-c",
|
||||||
|
"--config",
|
||||||
|
"config_file",
|
||||||
|
show_default=True,
|
||||||
|
default=utils.DEFAULT_CONFIG_FILE,
|
||||||
|
help="The aprsd config file to use for options.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--aprs-login",
|
||||||
|
envvar="APRS_LOGIN",
|
||||||
|
show_envvar=True,
|
||||||
|
help="What callsign to send the message from.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--aprs-password",
|
||||||
|
envvar="APRS_PASSWORD",
|
||||||
|
show_envvar=True,
|
||||||
|
help="the APRS-IS password for APRS_LOGIN",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--no-ack",
|
||||||
|
"-n",
|
||||||
|
is_flag=True,
|
||||||
|
show_default=True,
|
||||||
|
default=False,
|
||||||
|
help="Don't wait for an ack, just sent it to APRS-IS and bail.",
|
||||||
|
)
|
||||||
|
@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
|
||||||
|
@click.argument("tocallsign", required=False)
|
||||||
|
@click.argument("command", nargs=-1, required=False)
|
||||||
|
def listen(
|
||||||
|
loglevel,
|
||||||
|
quiet,
|
||||||
|
config_file,
|
||||||
|
aprs_login,
|
||||||
|
aprs_password,
|
||||||
|
no_ack,
|
||||||
|
raw,
|
||||||
|
tocallsign,
|
||||||
|
command,
|
||||||
|
):
|
||||||
|
"""Send a message to a callsign via APRS_IS."""
|
||||||
|
global got_ack, got_response
|
||||||
|
|
||||||
|
config = utils.parse_config(config_file)
|
||||||
|
if not aprs_login:
|
||||||
|
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not aprs_password:
|
||||||
|
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||||
|
return
|
||||||
|
|
||||||
|
config["aprs"]["login"] = aprs_login
|
||||||
|
config["aprs"]["password"] = aprs_password
|
||||||
|
messaging.CONFIG = config
|
||||||
|
|
||||||
|
setup_logging(config, loglevel, quiet)
|
||||||
|
LOG.info("APRSD TEST Started version: {}".format(aprsd.__version__))
|
||||||
|
if type(command) is tuple:
|
||||||
|
command = " ".join(command)
|
||||||
|
if not quiet:
|
||||||
|
if raw:
|
||||||
|
LOG.info("L'{}' R'{}'".format(aprs_login, raw))
|
||||||
|
else:
|
||||||
|
LOG.info("L'{}' To'{}' C'{}'".format(aprs_login, tocallsign, command))
|
||||||
|
|
||||||
|
flat_config = utils.flatten_dict(config)
|
||||||
|
LOG.info("Using CONFIG values:")
|
||||||
|
for x in flat_config:
|
||||||
|
if "password" in x or "aprsd.web.users.admin" in x:
|
||||||
|
LOG.info("{} = XXXXXXXXXXXXXXXXXXX".format(x))
|
||||||
|
else:
|
||||||
|
LOG.info("{} = {}".format(x, flat_config[x]))
|
||||||
|
|
||||||
|
got_ack = False
|
||||||
|
got_response = False
|
||||||
|
|
||||||
|
# TODO(walt) - manually edit this list
|
||||||
|
# prior to running aprsd-listen listen
|
||||||
|
watch_list = []
|
||||||
|
|
||||||
|
# build last seen list
|
||||||
|
last_seen = {}
|
||||||
|
for callsign in watch_list:
|
||||||
|
call = callsign.replace("*", "")
|
||||||
|
last_seen[call] = datetime.datetime.now()
|
||||||
|
|
||||||
|
LOG.debug("Last seen list")
|
||||||
|
LOG.debug(last_seen)
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
|
def rx_packet(packet):
|
||||||
|
global got_ack, got_response
|
||||||
|
LOG.debug("Got packet back {}".format(packet["raw"]))
|
||||||
|
|
||||||
|
if packet["from"] in last_seen:
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
age = str(now - last_seen[packet["from"]])
|
||||||
|
|
||||||
|
delta = utils.parse_delta_str(age)
|
||||||
|
d = datetime.timedelta(**delta)
|
||||||
|
|
||||||
|
max_timeout = {
|
||||||
|
"seconds": config["aprsd"]["watch_list"]["alert_time_seconds"],
|
||||||
|
}
|
||||||
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
|
if d > max_delta:
|
||||||
|
LOG.debug(
|
||||||
|
"NOTIFY!!! {} last seen {} max age={}".format(
|
||||||
|
packet["from"],
|
||||||
|
age,
|
||||||
|
max_delta,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
LOG.debug("Not old enough to notify {} < {}".format(d, max_delta))
|
||||||
|
LOG.debug("Update last seen from {}".format(packet["from"]))
|
||||||
|
last_seen[packet["from"]] = now
|
||||||
|
else:
|
||||||
|
LOG.debug(
|
||||||
|
"ignoring packet because {} not in watch list".format(packet["from"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = packet.get("response", None)
|
||||||
|
if resp == "ack":
|
||||||
|
ack_num = packet.get("msgNo")
|
||||||
|
LOG.info("We saw an ACK {} Ignoring".format(ack_num))
|
||||||
|
# messaging.log_packet(packet)
|
||||||
|
got_ack = True
|
||||||
|
else:
|
||||||
|
message = packet.get("message_text", None)
|
||||||
|
fromcall = packet["from"]
|
||||||
|
msg_number = packet.get("msgNo", "0")
|
||||||
|
messaging.log_message(
|
||||||
|
"Received Message",
|
||||||
|
packet["raw"],
|
||||||
|
message,
|
||||||
|
fromcall=fromcall,
|
||||||
|
ack=msg_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cl = client.Client(config)
|
||||||
|
cl.setup_connection()
|
||||||
|
except LoginError:
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
aprs_client = client.get_client()
|
||||||
|
|
||||||
|
# filter_str = 'b/{}'.format('/'.join(watch_list))
|
||||||
|
# LOG.debug("Filter by '{}'".format(filter_str))
|
||||||
|
# aprs_client.set_filter(filter_str)
|
||||||
|
filter_str = "p/{}".format("/".join(watch_list))
|
||||||
|
LOG.debug("Filter by '{}'".format(filter_str))
|
||||||
|
aprs_client.set_filter(filter_str)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# This will register a packet consumer with aprslib
|
||||||
|
# When new packets come in the consumer will process
|
||||||
|
# the packet
|
||||||
|
aprs_client.consumer(rx_packet, raw=False)
|
||||||
|
except aprslib.exceptions.ConnectionDrop:
|
||||||
|
LOG.error("Connection dropped, reconnecting")
|
||||||
|
time.sleep(5)
|
||||||
|
# Force the deletion of the client object connected to aprs
|
||||||
|
# This will cause a reconnect, next time client.get_client()
|
||||||
|
# is called
|
||||||
|
cl.reset()
|
||||||
|
except aprslib.exceptions.UnknownFormat:
|
||||||
|
LOG.error("Got a shitty packet")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -504,12 +504,26 @@ def server(
|
|||||||
LOG.debug("Loading saved MsgTrack object.")
|
LOG.debug("Loading saved MsgTrack object.")
|
||||||
messaging.MsgTrack().load()
|
messaging.MsgTrack().load()
|
||||||
|
|
||||||
|
rx_notify_queue = queue.Queue(maxsize=20)
|
||||||
rx_msg_queue = queue.Queue(maxsize=20)
|
rx_msg_queue = queue.Queue(maxsize=20)
|
||||||
tx_msg_queue = queue.Queue(maxsize=20)
|
tx_msg_queue = queue.Queue(maxsize=20)
|
||||||
msg_queues = {"rx": rx_msg_queue, "tx": tx_msg_queue}
|
msg_queues = {
|
||||||
|
"rx": rx_msg_queue,
|
||||||
|
"tx": tx_msg_queue,
|
||||||
|
"notify": rx_notify_queue,
|
||||||
|
}
|
||||||
|
|
||||||
rx_thread = threads.APRSDRXThread(msg_queues=msg_queues, config=config)
|
rx_thread = threads.APRSDRXThread(msg_queues=msg_queues, config=config)
|
||||||
tx_thread = threads.APRSDTXThread(msg_queues=msg_queues, config=config)
|
tx_thread = threads.APRSDTXThread(msg_queues=msg_queues, config=config)
|
||||||
|
if "watch_list" in config["aprsd"] and config["aprsd"]["watch_list"].get(
|
||||||
|
"enabled",
|
||||||
|
True,
|
||||||
|
):
|
||||||
|
notify_thread = threads.APRSDNotifyThread(
|
||||||
|
msg_queues=msg_queues,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
notify_thread.start()
|
||||||
|
|
||||||
if email_enabled:
|
if email_enabled:
|
||||||
email_thread = email.APRSDEmailThread(msg_queues=msg_queues, config=config)
|
email_thread = email.APRSDEmailThread(msg_queues=msg_queues, config=config)
|
||||||
|
@ -186,11 +186,12 @@ class MessageCounter:
|
|||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
max_count = 9999
|
max_count = 9999
|
||||||
|
lock = None
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""Make this a singleton class."""
|
"""Make this a singleton class."""
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls, *args, **kwargs)
|
||||||
cls._instance.val = RawValue("i", 1)
|
cls._instance.val = RawValue("i", 1)
|
||||||
cls._instance.lock = threading.Lock()
|
cls._instance.lock = threading.Lock()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
@ -4,6 +4,10 @@ import time
|
|||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
PACKET_TYPE_MESSAGE = "message"
|
||||||
|
PACKET_TYPE_ACK = "ack"
|
||||||
|
PACKET_TYPE_MICE = "mic-e"
|
||||||
|
|
||||||
|
|
||||||
class PacketList:
|
class PacketList:
|
||||||
"""Class to track all of the packets rx'd and tx'd by aprsd."""
|
"""Class to track all of the packets rx'd and tx'd by aprsd."""
|
||||||
@ -28,3 +32,17 @@ class PacketList:
|
|||||||
now = time.time()
|
now = time.time()
|
||||||
ts = str(now).split(".")[0]
|
ts = str(now).split(".")[0]
|
||||||
self.packet_list[ts] = packet
|
self.packet_list[ts] = packet
|
||||||
|
|
||||||
|
|
||||||
|
def get_packet_type(packet):
|
||||||
|
"""Decode the packet type from the packet."""
|
||||||
|
|
||||||
|
msg_format = packet.get("format", None)
|
||||||
|
msg_response = packet.get("response", None)
|
||||||
|
if msg_format == "message":
|
||||||
|
packet_type = PACKET_TYPE_MESSAGE
|
||||||
|
elif msg_response == "ack":
|
||||||
|
packet_type = PACKET_TYPE_ACK
|
||||||
|
elif msg_format == "mic-e":
|
||||||
|
packet_type = PACKET_TYPE_MICE
|
||||||
|
return packet_type
|
||||||
|
150
aprsd/plugin.py
150
aprsd/plugin.py
@ -17,7 +17,7 @@ LOG = logging.getLogger("APRSD")
|
|||||||
hookspec = pluggy.HookspecMarker("aprsd")
|
hookspec = pluggy.HookspecMarker("aprsd")
|
||||||
hookimpl = pluggy.HookimplMarker("aprsd")
|
hookimpl = pluggy.HookimplMarker("aprsd")
|
||||||
|
|
||||||
CORE_PLUGINS = [
|
CORE_MESSAGE_PLUGINS = [
|
||||||
"aprsd.plugins.email.EmailPlugin",
|
"aprsd.plugins.email.EmailPlugin",
|
||||||
"aprsd.plugins.fortune.FortunePlugin",
|
"aprsd.plugins.fortune.FortunePlugin",
|
||||||
"aprsd.plugins.location.LocationPlugin",
|
"aprsd.plugins.location.LocationPlugin",
|
||||||
@ -29,17 +29,58 @@ CORE_PLUGINS = [
|
|||||||
"aprsd.plugins.version.VersionPlugin",
|
"aprsd.plugins.version.VersionPlugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CORE_NOTIFY_PLUGINS = [
|
||||||
|
"aprsd.plugins.notify.BaseNotifyPlugin",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class APRSDCommandSpec:
|
class APRSDCommandSpec:
|
||||||
"""A hook specification namespace."""
|
"""A hook specification namespace."""
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
def run(self, fromcall, message, ack):
|
def run(self, packet):
|
||||||
"""My special little hook that you can customize."""
|
"""My special little hook that you can customize."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class APRSDPluginBase(metaclass=abc.ABCMeta):
|
class APRSDNotificationPluginBase(metaclass=abc.ABCMeta):
|
||||||
|
"""Base plugin class for all notification ased plugins.
|
||||||
|
|
||||||
|
All these plugins will get every packet seen by APRSD's
|
||||||
|
registered list of HAM callsigns in the config file's
|
||||||
|
watch_list.
|
||||||
|
|
||||||
|
When you want to 'notify' something when a packet is seen
|
||||||
|
by a particular HAM callsign, write a plugin based off of
|
||||||
|
this class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
"""The aprsd config object is stored."""
|
||||||
|
self.config = config
|
||||||
|
self.message_counter = 0
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def run(self, packet):
|
||||||
|
return self.notify(packet)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def notify(self, packet):
|
||||||
|
"""This is the main method called when a packet is rx.
|
||||||
|
|
||||||
|
This will get called when a packet is seen by a callsign
|
||||||
|
registered in the watch list in the config file."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDMessagePluginBase(metaclass=abc.ABCMeta):
|
||||||
|
"""Base Message plugin class.
|
||||||
|
|
||||||
|
When you want to search for a particular command in an
|
||||||
|
APRSD message and send a direct reply, write a plugin
|
||||||
|
based off of this class.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
"""The aprsd config object is stored."""
|
"""The aprsd config object is stored."""
|
||||||
self.config = config
|
self.config = config
|
||||||
@ -65,13 +106,14 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||||||
return self.message_counter
|
return self.message_counter
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def run(self, fromcall, message, ack):
|
def run(self, packet):
|
||||||
|
message = packet.get("message_text", None)
|
||||||
if re.search(self.command_regex, message):
|
if re.search(self.command_regex, message):
|
||||||
self.message_counter += 1
|
self.message_counter += 1
|
||||||
return self.command(fromcall, message, ack)
|
return self.command(packet)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
"""This is the command that runs when the regex matches.
|
"""This is the command that runs when the regex matches.
|
||||||
|
|
||||||
To reply with a message over the air, return a string
|
To reply with a message over the air, return a string
|
||||||
@ -84,8 +126,11 @@ class PluginManager:
|
|||||||
# The singleton instance object for this class
|
# The singleton instance object for this class
|
||||||
_instance = None
|
_instance = None
|
||||||
|
|
||||||
# the pluggy PluginManager
|
# the pluggy PluginManager for all Message plugins
|
||||||
_pluggy_pm = None
|
_pluggy_msg_pm = None
|
||||||
|
|
||||||
|
# the pluggy PluginManager for all Notification plugins
|
||||||
|
_pluggy_notify_pm = None
|
||||||
|
|
||||||
# aprsd config dict
|
# aprsd config dict
|
||||||
config = None
|
config = None
|
||||||
@ -130,7 +175,10 @@ class PluginManager:
|
|||||||
|
|
||||||
def is_plugin(self, obj):
|
def is_plugin(self, obj):
|
||||||
for c in inspect.getmro(obj):
|
for c in inspect.getmro(obj):
|
||||||
if issubclass(c, APRSDPluginBase):
|
if issubclass(c, APRSDMessagePluginBase) or issubclass(
|
||||||
|
c,
|
||||||
|
APRSDNotificationPluginBase,
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@ -167,7 +215,7 @@ class PluginManager:
|
|||||||
obj = cls(**kwargs)
|
obj = cls(**kwargs)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def _load_plugin(self, plugin_name):
|
def _load_msg_plugin(self, plugin_name):
|
||||||
"""
|
"""
|
||||||
Given a python fully qualified class path.name,
|
Given a python fully qualified class path.name,
|
||||||
Try importing the path, then creating the object,
|
Try importing the path, then creating the object,
|
||||||
@ -177,42 +225,81 @@ class PluginManager:
|
|||||||
try:
|
try:
|
||||||
plugin_obj = self._create_class(
|
plugin_obj = self._create_class(
|
||||||
plugin_name,
|
plugin_name,
|
||||||
APRSDPluginBase,
|
APRSDMessagePluginBase,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
)
|
)
|
||||||
if plugin_obj:
|
if plugin_obj:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"Registering Command plugin '{}'({}) '{}'".format(
|
"Registering Message plugin '{}'({}) '{}'".format(
|
||||||
plugin_name,
|
plugin_name,
|
||||||
plugin_obj.version,
|
plugin_obj.version,
|
||||||
plugin_obj.command_regex,
|
plugin_obj.command_regex,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._pluggy_pm.register(plugin_obj)
|
self._pluggy_msg_pm.register(plugin_obj)
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.exception("Couldn't load plugin '{}'".format(plugin_name), ex)
|
||||||
|
|
||||||
|
def _load_notify_plugin(self, plugin_name):
|
||||||
|
"""
|
||||||
|
Given a python fully qualified class path.name,
|
||||||
|
Try importing the path, then creating the object,
|
||||||
|
then registering it as a aprsd Command Plugin
|
||||||
|
"""
|
||||||
|
plugin_obj = None
|
||||||
|
try:
|
||||||
|
plugin_obj = self._create_class(
|
||||||
|
plugin_name,
|
||||||
|
APRSDNotificationPluginBase,
|
||||||
|
config=self.config,
|
||||||
|
)
|
||||||
|
if plugin_obj:
|
||||||
|
LOG.info(
|
||||||
|
"Registering Notification plugin '{}'({})".format(
|
||||||
|
plugin_name,
|
||||||
|
plugin_obj.version,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self._pluggy_notify_pm.register(plugin_obj)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.exception("Couldn't load plugin '{}'".format(plugin_name), ex)
|
LOG.exception("Couldn't load plugin '{}'".format(plugin_name), ex)
|
||||||
|
|
||||||
def reload_plugins(self):
|
def reload_plugins(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
del self._pluggy_pm
|
del self._pluggy_msg_pm
|
||||||
|
del self._pluggy_notify_pm
|
||||||
self.setup_plugins()
|
self.setup_plugins()
|
||||||
|
|
||||||
def setup_plugins(self):
|
def setup_plugins(self):
|
||||||
"""Create the plugin manager and register plugins."""
|
"""Create the plugin manager and register plugins."""
|
||||||
|
|
||||||
LOG.info("Loading Core APRSD Command Plugins")
|
LOG.info("Loading APRSD Message Plugins")
|
||||||
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
|
enabled_msg_plugins = self.config["aprsd"].get("enabled_plugins", None)
|
||||||
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
self._pluggy_msg_pm = pluggy.PluginManager("aprsd")
|
||||||
self._pluggy_pm.add_hookspecs(APRSDCommandSpec)
|
self._pluggy_msg_pm.add_hookspecs(APRSDCommandSpec)
|
||||||
if enabled_plugins:
|
if enabled_msg_plugins:
|
||||||
for p_name in enabled_plugins:
|
for p_name in enabled_msg_plugins:
|
||||||
self._load_plugin(p_name)
|
self._load_msg_plugin(p_name)
|
||||||
else:
|
else:
|
||||||
# Enabled plugins isn't set, so we default to loading all of
|
# Enabled plugins isn't set, so we default to loading all of
|
||||||
# the core plugins.
|
# the core plugins.
|
||||||
for p_name in CORE_PLUGINS:
|
for p_name in CORE_MESSAGE_PLUGINS:
|
||||||
self._load_plugin(p_name)
|
self._load_plugin(p_name)
|
||||||
|
|
||||||
|
if self.config["aprsd"]["watch_list"].get("enabled", False):
|
||||||
|
LOG.info("Loading APRSD Notification Plugins")
|
||||||
|
enabled_notify_plugins = self.config["aprsd"]["watch_list"].get(
|
||||||
|
"enabled_plugins",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
self._pluggy_notify_pm = pluggy.PluginManager("aprsd")
|
||||||
|
self._pluggy_notify_pm.add_hookspecs(APRSDCommandSpec)
|
||||||
|
if enabled_notify_plugins:
|
||||||
|
for p_name in enabled_notify_plugins:
|
||||||
|
self._load_notify_plugin(p_name)
|
||||||
|
|
||||||
|
# FIXME(Walt) - no real need to support loading random python classes
|
||||||
|
# from a directory anymore. Need to remove this.
|
||||||
plugin_dir = self.config["aprsd"].get("plugin_dir", None)
|
plugin_dir = self.config["aprsd"].get("plugin_dir", None)
|
||||||
if plugin_dir:
|
if plugin_dir:
|
||||||
LOG.info("Trying to load custom plugins from '{}'".format(plugin_dir))
|
LOG.info("Trying to load custom plugins from '{}'".format(plugin_dir))
|
||||||
@ -221,8 +308,6 @@ class PluginManager:
|
|||||||
LOG.info("Discovered {} modules to load".format(len(plugins_list)))
|
LOG.info("Discovered {} modules to load".format(len(plugins_list)))
|
||||||
for o in plugins_list:
|
for o in plugins_list:
|
||||||
plugin_obj = None
|
plugin_obj = None
|
||||||
# not setting enabled plugins means load all?
|
|
||||||
plugin_obj = o["obj"]
|
|
||||||
|
|
||||||
if plugin_obj:
|
if plugin_obj:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
@ -238,14 +323,19 @@ class PluginManager:
|
|||||||
LOG.info("Skipping Custom Plugins directory.")
|
LOG.info("Skipping Custom Plugins directory.")
|
||||||
LOG.info("Completed Plugin Loading.")
|
LOG.info("Completed Plugin Loading.")
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, packet):
|
||||||
"""Execute all the pluguns run method."""
|
"""Execute all the pluguns run method."""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self._pluggy_pm.hook.run(*args, **kwargs)
|
return self._pluggy_msg_pm.hook.run(packet=packet)
|
||||||
|
|
||||||
def register(self, obj):
|
def notify(self, packet):
|
||||||
|
"""Execute all the notify pluguns run method."""
|
||||||
|
with self.lock:
|
||||||
|
return self._pluggy_notify_pm.hook.run(packet=packet)
|
||||||
|
|
||||||
|
def register_msg(self, obj):
|
||||||
"""Register the plugin."""
|
"""Register the plugin."""
|
||||||
self._pluggy_pm.register(obj)
|
self._pluggy_msg_pm.register(obj)
|
||||||
|
|
||||||
def get_plugins(self):
|
def get_msg_plugins(self):
|
||||||
return self._pluggy_pm.get_plugins()
|
return self._pluggy_msg_pm.get_plugins()
|
||||||
|
@ -7,7 +7,7 @@ from aprsd import email, messaging, plugin, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class EmailPlugin(plugin.APRSDPluginBase):
|
class EmailPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""Email Plugin."""
|
"""Email Plugin."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -19,8 +19,13 @@ class EmailPlugin(plugin.APRSDPluginBase):
|
|||||||
email_sent_dict = {}
|
email_sent_dict = {}
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
LOG.info("Email COMMAND")
|
LOG.info("Email COMMAND")
|
||||||
|
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
message = packet.get("message_text", None)
|
||||||
|
ack = packet.get("msgNo", "0")
|
||||||
|
|
||||||
reply = None
|
reply = None
|
||||||
if not self.config["aprsd"]["email"].get("enabled", False):
|
if not self.config["aprsd"]["email"].get("enabled", False):
|
||||||
LOG.debug("Email is not enabled in config file ignoring.")
|
LOG.debug("Email is not enabled in config file ignoring.")
|
||||||
|
@ -7,7 +7,7 @@ from aprsd import plugin, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class FortunePlugin(plugin.APRSDPluginBase):
|
class FortunePlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""Fortune."""
|
"""Fortune."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -15,8 +15,13 @@ class FortunePlugin(plugin.APRSDPluginBase):
|
|||||||
command_name = "fortune"
|
command_name = "fortune"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
LOG.info("FortunePlugin")
|
LOG.info("FortunePlugin")
|
||||||
|
|
||||||
|
# fromcall = packet.get("from")
|
||||||
|
# message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
|
|
||||||
reply = None
|
reply = None
|
||||||
|
|
||||||
fortune_path = shutil.which("fortune")
|
fortune_path = shutil.which("fortune")
|
||||||
|
@ -7,7 +7,7 @@ from aprsd import plugin, plugin_utils, trace, utils
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class LocationPlugin(plugin.APRSDPluginBase):
|
class LocationPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""Location!"""
|
"""Location!"""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -15,8 +15,12 @@ class LocationPlugin(plugin.APRSDPluginBase):
|
|||||||
command_name = "location"
|
command_name = "location"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
LOG.info("Location Plugin")
|
LOG.info("Location Plugin")
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
|
|
||||||
# get last location of a callsign, get descriptive name from weather service
|
# get last location of a callsign, get descriptive name from weather service
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||||
|
23
aprsd/plugins/notify.py
Normal file
23
aprsd/plugins/notify.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from aprsd import packets, plugin, trace
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class BaseNotifyPlugin(plugin.APRSDNotificationPluginBase):
|
||||||
|
"""Notification base plugin."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
|
def notify(self, packet):
|
||||||
|
LOG.info("BaseNotifyPlugin")
|
||||||
|
|
||||||
|
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"]
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
|
||||||
|
packet_type = packets.get_packet_type(packet)
|
||||||
|
# we shouldn't notify the alert user that they are online.
|
||||||
|
if fromcall != notify_callsign:
|
||||||
|
return "{} was just seen by type:'{}'".format(fromcall, packet_type)
|
@ -6,7 +6,7 @@ from aprsd import plugin, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class PingPlugin(plugin.APRSDPluginBase):
|
class PingPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""Ping."""
|
"""Ping."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -14,8 +14,11 @@ class PingPlugin(plugin.APRSDPluginBase):
|
|||||||
command_name = "ping"
|
command_name = "ping"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
LOG.info("PINGPlugin")
|
LOG.info("PINGPlugin")
|
||||||
|
# fromcall = packet.get("from")
|
||||||
|
# message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
stm = time.localtime()
|
stm = time.localtime()
|
||||||
h = stm.tm_hour
|
h = stm.tm_hour
|
||||||
m = stm.tm_min
|
m = stm.tm_min
|
||||||
|
@ -7,7 +7,7 @@ from aprsd import messaging, plugin, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class QueryPlugin(plugin.APRSDPluginBase):
|
class QueryPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""Query command."""
|
"""Query command."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -15,9 +15,13 @@ class QueryPlugin(plugin.APRSDPluginBase):
|
|||||||
command_name = "query"
|
command_name = "query"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
LOG.info("Query COMMAND")
|
LOG.info("Query COMMAND")
|
||||||
|
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
|
|
||||||
tracker = messaging.MsgTrack()
|
tracker = messaging.MsgTrack()
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
reply = "Pending messages ({}) {}".format(
|
reply = "Pending messages ({}) {}".format(
|
||||||
|
@ -7,7 +7,7 @@ import yfinance as yf
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class StockPlugin(plugin.APRSDPluginBase):
|
class StockPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""Stock market plugin for fetching stock quotes"""
|
"""Stock market plugin for fetching stock quotes"""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -15,9 +15,13 @@ class StockPlugin(plugin.APRSDPluginBase):
|
|||||||
command_name = "stock"
|
command_name = "stock"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
LOG.info("StockPlugin")
|
LOG.info("StockPlugin")
|
||||||
|
|
||||||
|
# fromcall = packet.get("from")
|
||||||
|
message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
|
|
||||||
a = re.search(r"^.*\s+(.*)", message)
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
if a is not None:
|
if a is not None:
|
||||||
searchcall = a.group(1)
|
searchcall = a.group(1)
|
||||||
|
@ -8,7 +8,7 @@ import pytz
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class TimePlugin(plugin.APRSDPluginBase):
|
class TimePlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""Time command."""
|
"""Time command."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -39,7 +39,7 @@ class TimePlugin(plugin.APRSDPluginBase):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
LOG.info("TIME COMMAND")
|
LOG.info("TIME COMMAND")
|
||||||
# So we can mock this in unit tests
|
# So we can mock this in unit tests
|
||||||
localzone = self._get_local_tz()
|
localzone = self._get_local_tz()
|
||||||
@ -54,7 +54,11 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
|||||||
command_name = "Time"
|
command_name = "Time"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
# message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
|
|
||||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||||
try:
|
try:
|
||||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||||
@ -95,7 +99,10 @@ class TimeOWMPlugin(TimePlugin):
|
|||||||
command_name = "Time"
|
command_name = "Time"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
# message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||||
try:
|
try:
|
||||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||||
|
@ -6,7 +6,7 @@ from aprsd import plugin, stats, trace
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class VersionPlugin(plugin.APRSDPluginBase):
|
class VersionPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""Version of APRSD Plugin."""
|
"""Version of APRSD Plugin."""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
@ -18,8 +18,11 @@ class VersionPlugin(plugin.APRSDPluginBase):
|
|||||||
email_sent_dict = {}
|
email_sent_dict = {}
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
LOG.info("Version COMMAND")
|
LOG.info("Version COMMAND")
|
||||||
|
# fromcall = packet.get("from")
|
||||||
|
# message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
stats_obj = stats.APRSDStats()
|
stats_obj = stats.APRSDStats()
|
||||||
s = stats_obj.stats()
|
s = stats_obj.stats()
|
||||||
return "APRSD ver:{} uptime:{}".format(
|
return "APRSD ver:{} uptime:{}".format(
|
||||||
|
@ -8,7 +8,7 @@ import requests
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class USWeatherPlugin(plugin.APRSDPluginBase):
|
class USWeatherPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""USWeather Command
|
"""USWeather Command
|
||||||
|
|
||||||
Returns a weather report for the calling weather station
|
Returns a weather report for the calling weather station
|
||||||
@ -26,8 +26,11 @@ class USWeatherPlugin(plugin.APRSDPluginBase):
|
|||||||
command_name = "weather"
|
command_name = "weather"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
LOG.info("Weather Plugin")
|
LOG.info("Weather Plugin")
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
# message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@ -66,7 +69,7 @@ class USWeatherPlugin(plugin.APRSDPluginBase):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
class USMetarPlugin(plugin.APRSDPluginBase):
|
class USMetarPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""METAR Command
|
"""METAR Command
|
||||||
|
|
||||||
This provides a METAR weather report from a station near the caller
|
This provides a METAR weather report from a station near the caller
|
||||||
@ -86,7 +89,10 @@ class USMetarPlugin(plugin.APRSDPluginBase):
|
|||||||
command_name = "Metar"
|
command_name = "Metar"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
LOG.info("WX Plugin '{}'".format(message))
|
LOG.info("WX Plugin '{}'".format(message))
|
||||||
a = re.search(r"^.*\s+(.*)", message)
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
if a is not None:
|
if a is not None:
|
||||||
@ -154,7 +160,7 @@ class USMetarPlugin(plugin.APRSDPluginBase):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
class OWMWeatherPlugin(plugin.APRSDPluginBase):
|
class OWMWeatherPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""OpenWeatherMap Weather Command
|
"""OpenWeatherMap Weather Command
|
||||||
|
|
||||||
This provides weather near the caller or callsign.
|
This provides weather near the caller or callsign.
|
||||||
@ -178,7 +184,10 @@ class OWMWeatherPlugin(plugin.APRSDPluginBase):
|
|||||||
command_name = "Weather"
|
command_name = "Weather"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
LOG.info("OWMWeather Plugin '{}'".format(message))
|
LOG.info("OWMWeather Plugin '{}'".format(message))
|
||||||
a = re.search(r"^.*\s+(.*)", message)
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
if a is not None:
|
if a is not None:
|
||||||
@ -271,7 +280,7 @@ class OWMWeatherPlugin(plugin.APRSDPluginBase):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
class AVWXWeatherPlugin(plugin.APRSDPluginBase):
|
class AVWXWeatherPlugin(plugin.APRSDMessagePluginBase):
|
||||||
"""AVWXWeatherMap Weather Command
|
"""AVWXWeatherMap Weather Command
|
||||||
|
|
||||||
Fetches a METAR weather report for the nearest
|
Fetches a METAR weather report for the nearest
|
||||||
@ -299,7 +308,10 @@ class AVWXWeatherPlugin(plugin.APRSDPluginBase):
|
|||||||
command_name = "Weather"
|
command_name = "Weather"
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def command(self, fromcall, message, ack):
|
def command(self, packet):
|
||||||
|
fromcall = packet.get("from")
|
||||||
|
message = packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
LOG.info("OWMWeather Plugin '{}'".format(message))
|
LOG.info("OWMWeather Plugin '{}'".format(message))
|
||||||
a = re.search(r"^.*\s+(.*)", message)
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
if a is not None:
|
if a is not None:
|
||||||
|
@ -33,6 +33,7 @@ class APRSDStats:
|
|||||||
|
|
||||||
_mem_current = 0
|
_mem_current = 0
|
||||||
_mem_peak = 0
|
_mem_peak = 0
|
||||||
|
_watch_list = {}
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
@ -169,6 +170,15 @@ class APRSDStats:
|
|||||||
with self.lock:
|
with self.lock:
|
||||||
self._email_thread_last_time = datetime.datetime.now()
|
self._email_thread_last_time = datetime.datetime.now()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def watch_list(self):
|
||||||
|
with self.lock:
|
||||||
|
return self._watch_list
|
||||||
|
|
||||||
|
def update_watch_list(self, watch_list):
|
||||||
|
with self.lock:
|
||||||
|
self._watch_list = watch_list
|
||||||
|
|
||||||
def stats(self):
|
def stats(self):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
if self._email_thread_last_time:
|
if self._email_thread_last_time:
|
||||||
@ -182,7 +192,7 @@ class APRSDStats:
|
|||||||
last_aprsis_keepalive = "never"
|
last_aprsis_keepalive = "never"
|
||||||
|
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
plugins = pm.get_plugins()
|
plugins = pm.get_msg_plugins()
|
||||||
plugin_stats = {}
|
plugin_stats = {}
|
||||||
|
|
||||||
def full_name_with_qualname(obj):
|
def full_name_with_qualname(obj):
|
||||||
@ -202,6 +212,7 @@ class APRSDStats:
|
|||||||
"memory_current_str": utils.human_size(self.memory),
|
"memory_current_str": utils.human_size(self.memory),
|
||||||
"memory_peak": self.memory_peak,
|
"memory_peak": self.memory_peak,
|
||||||
"memory_peak_str": utils.human_size(self.memory_peak),
|
"memory_peak_str": utils.human_size(self.memory_peak),
|
||||||
|
"watch_list": self.watch_list,
|
||||||
},
|
},
|
||||||
"aprs-is": {
|
"aprs-is": {
|
||||||
"server": self.aprsis_server,
|
"server": self.aprsis_server,
|
||||||
|
164
aprsd/threads.py
164
aprsd/threads.py
@ -6,7 +6,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import tracemalloc
|
import tracemalloc
|
||||||
|
|
||||||
from aprsd import client, messaging, packets, plugin, stats, trace, utils
|
from aprsd import client, messaging, packets, plugin, stats, utils
|
||||||
import aprslib
|
import aprslib
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -114,6 +114,96 @@ class KeepAliveThread(APRSDThread):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDNotifyThread(APRSDThread):
|
||||||
|
last_seen = {}
|
||||||
|
|
||||||
|
def __init__(self, msg_queues, config):
|
||||||
|
super().__init__("NOTIFY_MSG")
|
||||||
|
self.msg_queues = msg_queues
|
||||||
|
self.config = config
|
||||||
|
for callsign in config["aprsd"]["watch_list"].get("callsigns", []):
|
||||||
|
call = callsign.replace("*", "")
|
||||||
|
# FIXME(waboring) - we should fetch the last time we saw
|
||||||
|
# a beacon from a callsign or some other mechanism to find
|
||||||
|
# last time a message was seen by aprs-is. For now this
|
||||||
|
# is all we can do.
|
||||||
|
self.last_seen[call] = datetime.datetime.now()
|
||||||
|
self.update_stats()
|
||||||
|
|
||||||
|
def update_stats(self):
|
||||||
|
stats_seen = {}
|
||||||
|
for callsign in self.last_seen:
|
||||||
|
stats_seen[callsign] = str(self.last_seen[callsign])
|
||||||
|
|
||||||
|
stats.APRSDStats().update_watch_list(stats_seen)
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
try:
|
||||||
|
packet = self.msg_queues["notify"].get(timeout=5)
|
||||||
|
|
||||||
|
if packet["from"] in self.last_seen:
|
||||||
|
# We only notify if the last time a callsign was seen
|
||||||
|
# is older than the alert_time_seconds
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
age = str(now - self.last_seen[packet["from"]])
|
||||||
|
|
||||||
|
delta = utils.parse_delta_str(age)
|
||||||
|
d = datetime.timedelta(**delta)
|
||||||
|
|
||||||
|
watch_list_conf = self.config["aprsd"]["watch_list"]
|
||||||
|
max_timeout = {
|
||||||
|
"seconds": watch_list_conf["alert_time_seconds"],
|
||||||
|
}
|
||||||
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
|
|
||||||
|
if d > max_delta:
|
||||||
|
LOG.info(
|
||||||
|
"NOTIFY {} last seen {} max age={}".format(
|
||||||
|
packet["from"],
|
||||||
|
age,
|
||||||
|
max_delta,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# NOW WE RUN through the notify plugins.
|
||||||
|
# If they return a msg, then we queue it for sending.
|
||||||
|
pm = plugin.PluginManager()
|
||||||
|
results = pm.notify(packet)
|
||||||
|
for reply in results:
|
||||||
|
if reply is not messaging.NULL_MESSAGE:
|
||||||
|
LOG.debug("Sending '{}'".format(reply))
|
||||||
|
|
||||||
|
msg = messaging.TextMessage(
|
||||||
|
self.config["aprs"]["login"],
|
||||||
|
watch_list_conf["alert_callsign"],
|
||||||
|
reply,
|
||||||
|
)
|
||||||
|
self.msg_queues["tx"].put(msg)
|
||||||
|
else:
|
||||||
|
LOG.debug("Got NULL MESSAGE from plugin")
|
||||||
|
|
||||||
|
else:
|
||||||
|
LOG.debug(
|
||||||
|
"Not old enough to notify callsign {}: {} < {}".format(
|
||||||
|
packet["from"],
|
||||||
|
age,
|
||||||
|
max_delta,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
LOG.debug("Update last seen from {}".format(packet["from"]))
|
||||||
|
self.last_seen[packet["from"]] = now
|
||||||
|
else:
|
||||||
|
LOG.debug("Ignoring packet from {}".format(packet["from"]))
|
||||||
|
|
||||||
|
# Allows stats object to have latest info from the last_seen dict
|
||||||
|
self.update_stats()
|
||||||
|
LOG.debug("Packet processing complete")
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
# Continue to loop
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class APRSDRXThread(APRSDThread):
|
class APRSDRXThread(APRSDThread):
|
||||||
def __init__(self, msg_queues, config):
|
def __init__(self, msg_queues, config):
|
||||||
super().__init__("RX_MSG")
|
super().__init__("RX_MSG")
|
||||||
@ -127,6 +217,23 @@ class APRSDRXThread(APRSDThread):
|
|||||||
def loop(self):
|
def loop(self):
|
||||||
aprs_client = client.get_client()
|
aprs_client = client.get_client()
|
||||||
|
|
||||||
|
# if we have a watch list enabled, we need to add filtering
|
||||||
|
# to enable seeing packets from the watch list.
|
||||||
|
if "watch_list" in self.config["aprsd"] and self.config["aprsd"][
|
||||||
|
"watch_list"
|
||||||
|
].get("enabled", False):
|
||||||
|
# watch list is enabled
|
||||||
|
watch_list = self.config["aprsd"]["watch_list"].get(
|
||||||
|
"callsigns",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
# make sure the timeout is set or this doesn't work
|
||||||
|
if watch_list:
|
||||||
|
filter_str = "p/{}".format("/".join(watch_list))
|
||||||
|
aprs_client.set_filter(filter_str)
|
||||||
|
else:
|
||||||
|
LOG.warning("Watch list enabled, but no callsigns set.")
|
||||||
|
|
||||||
# setup the consumer of messages and block until a messages
|
# setup the consumer of messages and block until a messages
|
||||||
try:
|
try:
|
||||||
# This will register a packet consumer with aprslib
|
# This will register a packet consumer with aprslib
|
||||||
@ -189,7 +296,7 @@ class APRSDRXThread(APRSDThread):
|
|||||||
# Get singleton of the PM
|
# Get singleton of the PM
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
try:
|
try:
|
||||||
results = pm.run(fromcall=fromcall, message=message, ack=msg_id)
|
results = pm.run(packet)
|
||||||
for reply in results:
|
for reply in results:
|
||||||
found_command = True
|
found_command = True
|
||||||
# A plugin can return a null message flag which signals
|
# A plugin can return a null message flag which signals
|
||||||
@ -208,7 +315,7 @@ class APRSDRXThread(APRSDThread):
|
|||||||
LOG.debug("Got NULL MESSAGE from plugin")
|
LOG.debug("Got NULL MESSAGE from plugin")
|
||||||
|
|
||||||
if not found_command:
|
if not found_command:
|
||||||
plugins = pm.get_plugins()
|
plugins = pm.get_msg_plugins()
|
||||||
names = [x.command_name for x in plugins]
|
names = [x.command_name for x in plugins]
|
||||||
names.sort()
|
names.sort()
|
||||||
|
|
||||||
@ -237,30 +344,43 @@ class APRSDRXThread(APRSDThread):
|
|||||||
self.msg_queues["tx"].put(ack)
|
self.msg_queues["tx"].put(ack)
|
||||||
LOG.debug("Packet processing complete")
|
LOG.debug("Packet processing complete")
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def process_packet(self, packet):
|
def process_packet(self, packet):
|
||||||
"""Process a packet recieved from aprs-is server."""
|
"""Process a packet recieved from aprs-is server."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stats.APRSDStats().msgs_rx_inc()
|
LOG.debug("Adding packet to notify queue {}".format(packet["raw"]))
|
||||||
packets.PacketList().add(packet)
|
self.msg_queues["notify"].put(packet)
|
||||||
|
|
||||||
msg = packet.get("message_text", None)
|
# since we can see packets from anyone now with the
|
||||||
msg_format = packet.get("format", None)
|
# watch list, we need to filter messages directly only to us.
|
||||||
msg_response = packet.get("response", None)
|
tocall = packet.get("addresse", None)
|
||||||
if msg_format == "message" and msg:
|
if tocall == self.config["aprs"]["login"]:
|
||||||
# we want to send the message through the
|
stats.APRSDStats().msgs_rx_inc()
|
||||||
# plugins
|
packets.PacketList().add(packet)
|
||||||
self.process_message_packet(packet)
|
|
||||||
return
|
|
||||||
elif msg_response == "ack":
|
|
||||||
self.process_ack_packet(packet)
|
|
||||||
return
|
|
||||||
|
|
||||||
if msg_format == "mic-e":
|
msg = packet.get("message_text", None)
|
||||||
# process a mic-e packet
|
msg_format = packet.get("format", None)
|
||||||
self.process_mic_e_packet(packet)
|
msg_response = packet.get("response", None)
|
||||||
return
|
if msg_format == "message" and msg:
|
||||||
|
# we want to send the message through the
|
||||||
|
# plugins
|
||||||
|
self.process_message_packet(packet)
|
||||||
|
return
|
||||||
|
elif msg_response == "ack":
|
||||||
|
self.process_ack_packet(packet)
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg_format == "mic-e":
|
||||||
|
# process a mic-e packet
|
||||||
|
self.process_mic_e_packet(packet)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
LOG.debug(
|
||||||
|
"Packet wasn't meant for us '{}'. Ignoring packet to '{}'".format(
|
||||||
|
self.config["aprs"]["login"],
|
||||||
|
tocall,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
except (aprslib.ParseError, aprslib.UnknownFormat) as exp:
|
except (aprslib.ParseError, aprslib.UnknownFormat) as exp:
|
||||||
LOG.exception("Failed to parse packet from aprs-is", exp)
|
LOG.exception("Failed to parse packet from aprs-is", exp)
|
||||||
@ -274,7 +394,7 @@ class APRSDTXThread(APRSDThread):
|
|||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
try:
|
try:
|
||||||
msg = self.msg_queues["tx"].get(timeout=0.1)
|
msg = self.msg_queues["tx"].get(timeout=5)
|
||||||
packets.PacketList().add(msg.dict())
|
packets.PacketList().add(msg.dict())
|
||||||
msg.send()
|
msg.send()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
|
@ -6,6 +6,7 @@ import functools
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@ -32,9 +33,9 @@ DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
|||||||
|
|
||||||
# an example of what should be in the ~/.aprsd/config.yml
|
# an example of what should be in the ~/.aprsd/config.yml
|
||||||
DEFAULT_CONFIG_DICT = {
|
DEFAULT_CONFIG_DICT = {
|
||||||
"ham": {"callsign": "CALLSIGN"},
|
"ham": {"callsign": "NOCALL"},
|
||||||
"aprs": {
|
"aprs": {
|
||||||
"login": "CALLSIGN",
|
"login": "NOCALL",
|
||||||
"password": "00000",
|
"password": "00000",
|
||||||
"host": "rotate.aprs2.net",
|
"host": "rotate.aprs2.net",
|
||||||
"port": 14580,
|
"port": 14580,
|
||||||
@ -45,15 +46,24 @@ DEFAULT_CONFIG_DICT = {
|
|||||||
"dateformat": DEFAULT_DATE_FORMAT,
|
"dateformat": DEFAULT_DATE_FORMAT,
|
||||||
"trace": False,
|
"trace": False,
|
||||||
"plugin_dir": "~/.config/aprsd/plugins",
|
"plugin_dir": "~/.config/aprsd/plugins",
|
||||||
"enabled_plugins": plugin.CORE_PLUGINS,
|
"enabled_plugins": plugin.CORE_MESSAGE_PLUGINS,
|
||||||
"units": "imperial",
|
"units": "imperial",
|
||||||
|
"watch_list": {
|
||||||
|
"enabled": False,
|
||||||
|
# Who gets the alert?
|
||||||
|
"alert_callsign": "NOCALL",
|
||||||
|
# 43200 is 12 hours
|
||||||
|
"alert_time_seconds": 43200,
|
||||||
|
"callsigns": [],
|
||||||
|
"enabled_plugins": plugin.CORE_NOTIFY_PLUGINS,
|
||||||
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"logging_enabled": True,
|
"logging_enabled": True,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 8001,
|
"port": 8001,
|
||||||
"users": {
|
"users": {
|
||||||
"admin": "aprsd",
|
"admin": "password-here",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
@ -334,6 +344,13 @@ def parse_config(config_file):
|
|||||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
|
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if config["aprsd"]["watch_list"]["enabled"] is True:
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
["aprsd", "watch_list", "alert_callsign"],
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
|
||||||
|
)
|
||||||
|
|
||||||
if config["aprsd"]["email"]["enabled"] is True:
|
if config["aprsd"]["email"]["enabled"] is True:
|
||||||
# Check IMAP server settings
|
# Check IMAP server settings
|
||||||
check_option(config, ["aprsd", "email", "imap", "host"])
|
check_option(config, ["aprsd", "email", "imap", "host"])
|
||||||
@ -407,3 +424,14 @@ def flatten_dict(d, parent_key="", sep="."):
|
|||||||
else:
|
else:
|
||||||
items.append((new_key, v))
|
items.append((new_key, v))
|
||||||
return dict(items)
|
return dict(items)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_delta_str(s):
|
||||||
|
if "day" in s:
|
||||||
|
m = re.match(
|
||||||
|
r"(?P<days>[-\d]+) day[s]*, (?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)",
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
|
||||||
|
return {key: float(val) for key, val in m.groupdict().items()}
|
||||||
|
@ -177,7 +177,7 @@ function updateQuadData(chart, label, first, second, third, fourth) {
|
|||||||
|
|
||||||
function update_stats( data ) {
|
function update_stats( data ) {
|
||||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
$("#version").text( data["stats"]["aprsd"]["version"] );
|
||||||
$("#aprsis").text( "APRS-IS Server: " + data["stats"]["aprs-is"]["server"] );
|
$("#aprsis").html( "APRS-IS Server: <a href='http://status.aprs2.net' >" + data["stats"]["aprs-is"]["server"] + "</a>" );
|
||||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
||||||
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
||||||
$("#jsonstats").html(html_pretty);
|
$("#jsonstats").html(html_pretty);
|
||||||
@ -185,6 +185,16 @@ function update_stats( data ) {
|
|||||||
updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["recieved"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["recieved"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
||||||
updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
||||||
updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
||||||
|
|
||||||
|
// Update the watch list
|
||||||
|
var watchdiv = $("#watchDiv");
|
||||||
|
var html_str = '<table class="ui celled striped table"><thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th></tr></thead><tbody>'
|
||||||
|
watchdiv.html('')
|
||||||
|
jQuery.each(data["stats"]["aprsd"]["watch_list"], function(i, val) {
|
||||||
|
html_str += '<tr><td class="collapsing"><i class="phone volume icon"></i>' + i + '</td><td>' + val + '</td></tr>'
|
||||||
|
});
|
||||||
|
html_str += "</tbody></table>";
|
||||||
|
watchdiv.append(html_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,6 +68,7 @@
|
|||||||
<div class="ui top attached tabular menu">
|
<div class="ui top attached tabular menu">
|
||||||
<div class="active item" data-tab="charts-tab">Charts</div>
|
<div class="active item" data-tab="charts-tab">Charts</div>
|
||||||
<div class="item" data-tab="msgs-tab">Messages</div>
|
<div class="item" data-tab="msgs-tab">Messages</div>
|
||||||
|
<div class="item" data-tab="watch-tab">Watch List</div>
|
||||||
<div class="item" data-tab="config-tab">Config</div>
|
<div class="item" data-tab="config-tab">Config</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -94,6 +95,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="ui bottom attached tab segment" data-tab="watch-tab">
|
||||||
|
<h3 class="ui dividing header">
|
||||||
|
Callsign Watch List (<span id="watch_count">{{ watch_count }}</span>)
|
||||||
|
|
||||||
|
Notification age - <span id="watch_age">{{ watch_age }}</span>
|
||||||
|
</h3>
|
||||||
|
<div id="watchDiv" class="ui mini text">Loading</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="config-tab">
|
<div class="ui bottom attached tab segment" data-tab="config-tab">
|
||||||
<h3 class="ui dividing header">Config</h3>
|
<h3 class="ui dividing header">Config</h3>
|
||||||
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
||||||
|
@ -34,6 +34,7 @@ packages =
|
|||||||
[entry_points]
|
[entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
aprsd = aprsd.main:main
|
aprsd = aprsd.main:main
|
||||||
|
aprsd-listen = aprsd.listen:main
|
||||||
aprsd-dev = aprsd.dev:main
|
aprsd-dev = aprsd.dev:main
|
||||||
aprsd-healthcheck = aprsd.healthcheck:main
|
aprsd-healthcheck = aprsd.healthcheck:main
|
||||||
fake_aprs = aprsd.fake_aprs:main
|
fake_aprs = aprsd.fake_aprs:main
|
||||||
|
@ -21,13 +21,23 @@ class TestPlugin(unittest.TestCase):
|
|||||||
# Inintialize the stats object with the config
|
# Inintialize the stats object with the config
|
||||||
stats.APRSDStats(self.config)
|
stats.APRSDStats(self.config)
|
||||||
|
|
||||||
|
def fake_packet(self, fromcall="KFART", message=None, msg_number=None):
|
||||||
|
packet = {"from": fromcall}
|
||||||
|
if message:
|
||||||
|
packet["message_text"] = message
|
||||||
|
|
||||||
|
if msg_number:
|
||||||
|
packet["msgNo"] = msg_number
|
||||||
|
|
||||||
|
return packet
|
||||||
|
|
||||||
@mock.patch("shutil.which")
|
@mock.patch("shutil.which")
|
||||||
def test_fortune_fail(self, mock_which):
|
def test_fortune_fail(self, mock_which):
|
||||||
fortune = fortune_plugin.FortunePlugin(self.config)
|
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||||
mock_which.return_value = None
|
mock_which.return_value = None
|
||||||
message = "fortune"
|
|
||||||
expected = "Fortune command not installed"
|
expected = "Fortune command not installed"
|
||||||
actual = fortune.run(self.fromcall, message, self.ack)
|
packet = self.fake_packet(message="fortune")
|
||||||
|
actual = fortune.run(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("subprocess.check_output")
|
@mock.patch("subprocess.check_output")
|
||||||
@ -38,18 +48,18 @@ class TestPlugin(unittest.TestCase):
|
|||||||
|
|
||||||
mock_output.return_value = "Funny fortune"
|
mock_output.return_value = "Funny fortune"
|
||||||
|
|
||||||
message = "fortune"
|
|
||||||
expected = "Funny fortune"
|
expected = "Funny fortune"
|
||||||
actual = fortune.run(self.fromcall, message, self.ack)
|
packet = self.fake_packet(message="fortune")
|
||||||
|
actual = fortune.run(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("aprsd.messaging.MsgTrack.flush")
|
@mock.patch("aprsd.messaging.MsgTrack.flush")
|
||||||
def test_query_flush(self, mock_flush):
|
def test_query_flush(self, mock_flush):
|
||||||
message = "!delete"
|
packet = self.fake_packet(message="!delete")
|
||||||
query = query_plugin.QueryPlugin(self.config)
|
query = query_plugin.QueryPlugin(self.config)
|
||||||
|
|
||||||
expected = "Deleted ALL pending msgs."
|
expected = "Deleted ALL pending msgs."
|
||||||
actual = query.run(self.fromcall, message, self.ack)
|
actual = query.run(packet)
|
||||||
mock_flush.assert_called_once()
|
mock_flush.assert_called_once()
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@ -57,11 +67,11 @@ class TestPlugin(unittest.TestCase):
|
|||||||
def test_query_restart_delayed(self, mock_restart):
|
def test_query_restart_delayed(self, mock_restart):
|
||||||
track = messaging.MsgTrack()
|
track = messaging.MsgTrack()
|
||||||
track.track = {}
|
track.track = {}
|
||||||
message = "!4"
|
packet = self.fake_packet(message="!4")
|
||||||
query = query_plugin.QueryPlugin(self.config)
|
query = query_plugin.QueryPlugin(self.config)
|
||||||
|
|
||||||
expected = "No pending msgs to resend"
|
expected = "No pending msgs to resend"
|
||||||
actual = query.run(self.fromcall, message, self.ack)
|
actual = query.run(packet)
|
||||||
mock_restart.assert_not_called()
|
mock_restart.assert_not_called()
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
mock_restart.reset_mock()
|
mock_restart.reset_mock()
|
||||||
@ -69,7 +79,7 @@ class TestPlugin(unittest.TestCase):
|
|||||||
# add a message
|
# add a message
|
||||||
msg = messaging.TextMessage(self.fromcall, "testing", self.ack)
|
msg = messaging.TextMessage(self.fromcall, "testing", self.ack)
|
||||||
track.add(msg)
|
track.add(msg)
|
||||||
actual = query.run(self.fromcall, message, self.ack)
|
actual = query.run(packet)
|
||||||
mock_restart.assert_called_once()
|
mock_restart.assert_called_once()
|
||||||
|
|
||||||
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
|
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
|
||||||
@ -89,22 +99,28 @@ class TestPlugin(unittest.TestCase):
|
|||||||
fake_time.tm_sec = 13
|
fake_time.tm_sec = 13
|
||||||
time = time_plugin.TimePlugin(self.config)
|
time = time_plugin.TimePlugin(self.config)
|
||||||
|
|
||||||
fromcall = "KFART"
|
packet = self.fake_packet(
|
||||||
message = "location"
|
fromcall="KFART",
|
||||||
ack = 1
|
message="location",
|
||||||
|
msg_number=1,
|
||||||
|
)
|
||||||
|
|
||||||
actual = time.run(fromcall, message, ack)
|
actual = time.run(packet)
|
||||||
self.assertEqual(None, actual)
|
self.assertEqual(None, actual)
|
||||||
|
|
||||||
cur_time = fuzzy(h, m, 1)
|
cur_time = fuzzy(h, m, 1)
|
||||||
|
|
||||||
message = "time"
|
packet = self.fake_packet(
|
||||||
|
fromcall="KFART",
|
||||||
|
message="time",
|
||||||
|
msg_number=1,
|
||||||
|
)
|
||||||
local_short_str = local_t.strftime("%H:%M %Z")
|
local_short_str = local_t.strftime("%H:%M %Z")
|
||||||
expected = "{} ({})".format(
|
expected = "{} ({})".format(
|
||||||
cur_time,
|
cur_time,
|
||||||
local_short_str,
|
local_short_str,
|
||||||
)
|
)
|
||||||
actual = time.run(fromcall, message, ack)
|
actual = time.run(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("time.localtime")
|
@mock.patch("time.localtime")
|
||||||
@ -117,11 +133,13 @@ class TestPlugin(unittest.TestCase):
|
|||||||
|
|
||||||
ping = ping_plugin.PingPlugin(self.config)
|
ping = ping_plugin.PingPlugin(self.config)
|
||||||
|
|
||||||
fromcall = "KFART"
|
packet = self.fake_packet(
|
||||||
message = "location"
|
fromcall="KFART",
|
||||||
ack = 1
|
message="location",
|
||||||
|
msg_number=1,
|
||||||
|
)
|
||||||
|
|
||||||
result = ping.run(fromcall, message, ack)
|
result = ping.run(packet)
|
||||||
self.assertEqual(None, result)
|
self.assertEqual(None, result)
|
||||||
|
|
||||||
def ping_str(h, m, s):
|
def ping_str(h, m, s):
|
||||||
@ -134,31 +152,49 @@ class TestPlugin(unittest.TestCase):
|
|||||||
+ str(s).zfill(2)
|
+ str(s).zfill(2)
|
||||||
)
|
)
|
||||||
|
|
||||||
message = "Ping"
|
packet = self.fake_packet(
|
||||||
actual = ping.run(fromcall, message, ack)
|
fromcall="KFART",
|
||||||
|
message="Ping",
|
||||||
|
msg_number=1,
|
||||||
|
)
|
||||||
|
actual = ping.run(packet)
|
||||||
expected = ping_str(h, m, s)
|
expected = ping_str(h, m, s)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
message = "ping"
|
packet = self.fake_packet(
|
||||||
actual = ping.run(fromcall, message, ack)
|
fromcall="KFART",
|
||||||
|
message="ping",
|
||||||
|
msg_number=1,
|
||||||
|
)
|
||||||
|
actual = ping.run(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("aprsd.plugin.PluginManager.get_plugins")
|
@mock.patch("aprsd.plugin.PluginManager.get_msg_plugins")
|
||||||
def test_version(self, mock_get_plugins):
|
def test_version(self, mock_get_plugins):
|
||||||
expected = "APRSD ver:{} uptime:0:0:0".format(aprsd.__version__)
|
expected = "APRSD ver:{} uptime:0:0:0".format(aprsd.__version__)
|
||||||
version = version_plugin.VersionPlugin(self.config)
|
version = version_plugin.VersionPlugin(self.config)
|
||||||
|
|
||||||
fromcall = "KFART"
|
packet = self.fake_packet(
|
||||||
message = "No"
|
fromcall="KFART",
|
||||||
ack = 1
|
message="No",
|
||||||
|
msg_number=1,
|
||||||
|
)
|
||||||
|
|
||||||
actual = version.run(fromcall, message, ack)
|
actual = version.run(packet)
|
||||||
self.assertEqual(None, actual)
|
self.assertEqual(None, actual)
|
||||||
|
|
||||||
message = "version"
|
packet = self.fake_packet(
|
||||||
actual = version.run(fromcall, message, ack)
|
fromcall="KFART",
|
||||||
|
message="version",
|
||||||
|
msg_number=1,
|
||||||
|
)
|
||||||
|
actual = version.run(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
message = "Version"
|
packet = self.fake_packet(
|
||||||
actual = version.run(fromcall, message, ack)
|
fromcall="KFART",
|
||||||
|
message="Version",
|
||||||
|
msg_number=1,
|
||||||
|
)
|
||||||
|
actual = version.run(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
Loading…
Reference in New Issue
Block a user