diff --git a/aprsd/dev.py b/aprsd/dev.py index 0c153fe..aecb2c3 100644 --- a/aprsd/dev.py +++ b/aprsd/dev.py @@ -178,12 +178,22 @@ def setup_logging(config, loglevel, quiet): default="aprsd.plugins.wx.WxPlugin", help="The plugin to run", ) +@click.option( + "-a", + "--all", + "load_all", + show_default=True, + is_flag=True, + default=False, + help="Load all the plugins in config?", +) @click.argument("fromcall") @click.argument("message", nargs=-1, required=True) def test_plugin( loglevel, config_file, plugin_path, + load_all, fromcall, message, ): @@ -199,8 +209,12 @@ def test_plugin( client.Client(config) pm = plugin.PluginManager(config) - pm._init() + if load_all: + pm.setup_plugins() + else: + pm._init() obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) + # Register the plugin they wanted tested. pm._pluggy_pm.register(obj) login = config["aprs"]["login"] @@ -212,8 +226,9 @@ def test_plugin( } reply = pm.run(packet) + pm.stop() # Plugin might have threads, so lets stop them so we can exit. - obj.stop_threads() + # obj.stop_threads() LOG.info(f"Result = '{reply}'") diff --git a/aprsd/plugin.py b/aprsd/plugin.py index 2e95020..a141ffe 100644 --- a/aprsd/plugin.py +++ b/aprsd/plugin.py @@ -6,6 +6,7 @@ import inspect import logging import os import re +import textwrap import threading import pluggy @@ -62,7 +63,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta): self.config = config self.message_counter = 0 self.setup() - self.threads = self.create_threads() + self.threads = self.create_threads() or [] self.start_threads() def start_threads(self): @@ -93,6 +94,9 @@ class APRSDPluginBase(metaclass=abc.ABCMeta): def message_count(self): return self.message_counter + def help(self): + return "Help!" + @abc.abstractmethod def setup(self): """Do any plugin setup here.""" @@ -198,6 +202,12 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): """The regex to match from the caller""" raise NotImplementedError + def help(self): + return "{}: {}".format( + self.command_name.lower(), + self.command_regex, + ) + def setup(self): """Do any plugin setup here.""" self.enabled = True @@ -228,6 +238,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): self.__class__, ex, ), ) + LOG.exception(ex) if result: self.tx_inc() else: @@ -236,6 +247,66 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta): return result +class HelpPlugin(APRSDRegexCommandPluginBase): + """Help Plugin that is always enabled. + + This plugin is in this file to prevent a circular import. + """ + + version = "1.0" + command_regex = "^[hH]" + command_name = "help" + + def help(self): + return "Help: send APRS help or help " + + def process(self, packet): + LOG.info("HelpPlugin") + # fromcall = packet.get("from") + message = packet.get("message_text", None) + # ack = packet.get("msgNo", "0") + a = re.search(r"^.*\s+(.*)", message) + command_name = None + if a is not None: + command_name = a.group(1).lower() + + pm = PluginManager() + + if command_name and "?" not in command_name: + # user wants help for a specific plugin + reply = None + for p in pm.get_plugins(): + if ( + p.enabled and isinstance(p, APRSDRegexCommandPluginBase) + and p.command_name.lower() == command_name + ): + reply = p.help() + + if reply: + return reply + + list = [] + for p in pm.get_plugins(): + LOG.debug(p) + if p.enabled and isinstance(p, APRSDRegexCommandPluginBase): + name = p.command_name.lower() + if name not in list and "help" not in name: + list.append(name) + + list.sort() + reply = " ".join(list) + lines = textwrap.wrap(reply, 60) + replies = ["Send APRS MSG of 'help' or 'help '"] + for line in lines: + replies.append(f"plugins: {line}") + + for entry in replies: + LOG.debug(f"{len(entry)} {entry}") + + LOG.debug(f"{replies}") + return replies + + class PluginManager: # The singleton instance object for this class _instance = None @@ -365,8 +436,12 @@ class PluginManager: """Create the plugin manager and register plugins.""" LOG.info("Loading APRSD Plugins") - enabled_plugins = self.config["aprsd"].get("enabled_plugins", None) self._init() + # Help plugin is always enabled. + _help = HelpPlugin(self.config) + self._pluggy_pm.register(_help) + + enabled_plugins = self.config["aprsd"].get("enabled_plugins", None) if enabled_plugins: for p_name in enabled_plugins: self._load_plugin(p_name) @@ -392,6 +467,12 @@ class PluginManager: with self.lock: return self._pluggy_pm.hook.filter(packet=packet) + def stop(self): + """Stop all threads created by all plugins.""" + with self.lock: + for p in self.get_plugins(): + p.stop_threads() + def register_msg(self, obj): """Register the plugin.""" self._pluggy_pm.register(obj) diff --git a/aprsd/plugins/notify.py b/aprsd/plugins/notify.py index 8a31563..59adc5a 100644 --- a/aprsd/plugins/notify.py +++ b/aprsd/plugins/notify.py @@ -1,6 +1,6 @@ import logging -from aprsd import messaging, packets, plugin +from aprsd import packets, plugin LOG = logging.getLogger("APRSD") @@ -46,4 +46,3 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase): wl.max_delta(), ), ) - return messaging.NULL_MESSAGE diff --git a/aprsd/plugins/time.py b/aprsd/plugins/time.py index ad660ac..61e9f4b 100644 --- a/aprsd/plugins/time.py +++ b/aprsd/plugins/time.py @@ -54,7 +54,7 @@ class TimeOpenCageDataPlugin(TimePlugin): version = "1.0" command_regex = "^[tT]" - command_name = "Time" + command_name = "time" @trace.trace def process(self, packet): @@ -120,7 +120,7 @@ class TimeOWMPlugin(TimePlugin): version = "1.0" command_regex = "^[tT]" - command_name = "Time" + command_name = "time" @trace.trace def process(self, packet): diff --git a/aprsd/plugins/weather.py b/aprsd/plugins/weather.py index 21364ab..0817c8e 100644 --- a/aprsd/plugins/weather.py +++ b/aprsd/plugins/weather.py @@ -25,7 +25,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase): version = "1.0" command_regex = "^[wW]" - command_name = "weather" + command_name = "USWeather" @trace.trace def process(self, packet): @@ -88,7 +88,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase): version = "1.0" command_regex = "^[metar]" - command_name = "Metar" + command_name = "USMetar" @trace.trace def process(self, packet): @@ -180,7 +180,16 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase): version = "1.0" command_regex = "^[wW]" - command_name = "Weather" + command_name = "OpenWeatherMap" + + def help(self): + _help = [ + "openweathermap: Send {} to get weather " + "from your location".format(self.command_regex), + "openweathermap: Send {} to get " + "weather from ".format(self.command_regex), + ] + return _help @trace.trace def process(self, packet): @@ -301,7 +310,16 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase): version = "1.0" command_regex = "^[mM]" - command_name = "Weather" + command_name = "AVWXWeather" + + def help(self): + _help = [ + "avwxweather: Send {} to get weather " + "from your location".format(self.command_regex), + "avwxweather: Send {} to get " + "weather from ".format(self.command_regex), + ] + return _help @trace.trace def process(self, packet): diff --git a/aprsd/threads.py b/aprsd/threads.py index 8cf75f0..3e6efb5 100644 --- a/aprsd/threads.py +++ b/aprsd/threads.py @@ -302,12 +302,11 @@ class APRSDProcessPacketThread(APRSDThread): # If the message was for us and we didn't have a # response, then we send a usage statement. if tocall == self.config["aprs"]["login"] and not replied: - reply = "Usage: weather, locate [call], time, fortune, ping" - + LOG.warning("Sending help!") msg = messaging.TextMessage( self.config["aprs"]["login"], fromcall, - reply, + "Unknown command! Send 'help' message for help", ) msg.send() except Exception as ex: