diff --git a/AUTHORS b/AUTHORS index cc71ac7..3a44899 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1,2 @@ Hemna +Walter A. Boring IV diff --git a/ChangeLog b/ChangeLog index 5874a5d..9e128b1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,16 +1,14 @@ CHANGES ======= -v0.1.4 ------- - +* don't dump the whole packet +* use Tx to send +* Working with pre 2.7.0 +* Removed trace * lint * Added pbr version - -v0.1.3 ------- - * Fixed missing entry in requirements.txt +* Create FUNDING.yml v0.1.2 ------ diff --git a/aprsd_weewx_plugin/conf/__init__.py b/aprsd_weewx_plugin/conf/__init__.py new file mode 100644 index 0000000..4a77677 --- /dev/null +++ b/aprsd_weewx_plugin/conf/__init__.py @@ -0,0 +1,10 @@ +import logging + +from oslo_config import cfg + +from aprsd_weewx_plugin.conf import weewx + + +CONF = cfg.CONF + +weewx.register_opts(CONF) diff --git a/aprsd_weewx_plugin/conf/opts.py b/aprsd_weewx_plugin/conf/opts.py new file mode 100644 index 0000000..4cf85d5 --- /dev/null +++ b/aprsd_weewx_plugin/conf/opts.py @@ -0,0 +1,80 @@ +# Copyright 2015 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +This is the single point of entry to generate the sample configuration +file for Nova. It collects all the necessary info from the other modules +in this package. It is assumed that: + +* every other module in this package has a 'list_opts' function which + return a dict where + * the keys are strings which are the group names + * the value of each key is a list of config options for that group +* the nova.conf package doesn't have further packages with config options +* this module is only used in the context of sample file generation +""" + +import collections +import importlib +import os +import pkgutil + + +LIST_OPTS_FUNC_NAME = "list_opts" + + +def _tupleize(dct): + """Take the dict of options and convert to the 2-tuple format.""" + return [(key, val) for key, val in dct.items()] + + +def list_opts(): + opts = collections.defaultdict(list) + module_names = _list_module_names() + imported_modules = _import_modules(module_names) + _append_config_options(imported_modules, opts) + return _tupleize(opts) + + +def _list_module_names(): + module_names = [] + package_path = os.path.dirname(os.path.abspath(__file__)) + for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]): + if modname == "opts" or ispkg: + continue + else: + module_names.append(modname) + return module_names + + +def _import_modules(module_names): + imported_modules = [] + for modname in module_names: + mod = importlib.import_module("aprsd_weewx_plugin.conf." + modname) + if not hasattr(mod, LIST_OPTS_FUNC_NAME): + msg = "The module 'aprsd_weewx_plugin.conf.%s' should have a '%s' "\ + "function which returns the config options." % \ + (modname, LIST_OPTS_FUNC_NAME) + raise Exception(msg) + else: + imported_modules.append(mod) + return imported_modules + + +def _append_config_options(imported_modules, config_options): + for mod in imported_modules: + configs = mod.list_opts() + for key, val in configs.items(): + config_options[key].extend(val) diff --git a/aprsd_weewx_plugin/conf/weewx.py b/aprsd_weewx_plugin/conf/weewx.py new file mode 100644 index 0000000..51a6bdf --- /dev/null +++ b/aprsd_weewx_plugin/conf/weewx.py @@ -0,0 +1,62 @@ +from oslo_config import cfg + + +weewx_group = cfg.OptGroup( + name="aprsd_weewx_plugin", + title="APRSD Weewx Plugin settings", +) + +weewx_opts = [ + cfg.FloatOpt( + "latitude", + default=None, + help="Latitude of the station you want to report as", + ), + cfg.FloatOpt( + "longitude", + default=None, + help="Longitude of the station you want to report as", + ), + cfg.IntOpt( + "report_interval", + default=60, + help="How long (in seconds) in between weather reports", + ), +] + +weewx_mqtt_opts = [ + cfg.StrOpt( + "mqtt_user", + help="MQTT username", + ), + cfg.StrOpt( + "mqtt_password", + secret=True, + help="MQTT password", + ), + cfg.StrOpt( + "mqtt_host", + help="MQTT Hostname to connect to", + ), + cfg.PortOpt( + "mqtt_port", + help="MQTT Port", + ), +] + +ALL_OPTS = ( + weewx_opts + + weewx_mqtt_opts +) + + +def register_opts(cfg): + cfg.register_group(weewx_group) + cfg.register_opts(ALL_OPTS, group=weewx_group) + + +def list_opts(): + register_opts(cfg.CONF) + return { + weewx_group.name: ALL_OPTS, + } diff --git a/aprsd_weewx_plugin/weewx.py b/aprsd_weewx_plugin/weewx.py index c0cf4cb..272ad99 100644 --- a/aprsd_weewx_plugin/weewx.py +++ b/aprsd_weewx_plugin/weewx.py @@ -9,8 +9,12 @@ import aprsd.messaging import paho.mqtt.client as mqtt from aprsd import plugin, threads from aprsd.threads import tx +from oslo_config import cfg + +from aprsd_weewx_plugin import conf # noqa +CONF = cfg.CONF LOG = logging.getLogger("APRSD") @@ -41,21 +45,22 @@ class WeewxMQTTPlugin(plugin.APRSDRegexCommandPluginBase): def setup(self): """Ensure that the plugin has been configured.""" - LOG.info("Looking for weewx.mqtt.host config entry") - if self.config.exists("services.weewx.mqtt.host"): - self.enabled = True - else: - LOG.error("Failed to find config weewx:mqtt:host") - LOG.info("Disabling the weewx mqtt subsription thread.") + self.enabled = True + if not CONF.aprsd_weewx_plugin.mqtt_host: + LOG.error("aprsd_weewx_plugin.mqtt_host is not set in config!") self.enabled = False + if not CONF.aprsd_weewx_plugin.mqtt_user: + LOG.warning("aprsd_weewx_plugin.mqtt_user is not set") + if not CONF.aprsd_weewx_plugin.mqtt_password: + LOG.warning("aprsd_weewx_plugin.mqtt_password is not set") + def create_threads(self): if self.enabled: LOG.info("Creating WeewxMQTTThread") self.queue = ClearableQueue(maxsize=1) self.wx_queue = ClearableQueue(maxsize=1) mqtt_thread = WeewxMQTTThread( - config=self.config, wx_queue=self.wx_queue, msg_queue=self.queue, ) @@ -65,19 +70,21 @@ class WeewxMQTTPlugin(plugin.APRSDRegexCommandPluginBase): # Then we can periodically report weather data # to APRS if ( - self.config.exists("services.weewx.location.latitude") and - self.config.exists("services.weewx.location.longitude") + CONF.aprsd_weewx_plugin.latitude and + CONF.aprsd_weewx_plugin.longitude ): LOG.info("Creating WeewxWXAPRSThread") wx_thread = WeewxWXAPRSThread( - config=self.config, wx_queue=self.wx_queue, ) threads.append(wx_thread) else: LOG.info( "NOT starting Weewx WX APRS Thread due to missing " - "GPS location settings.", + "GPS location settings. Please set " + "aprsd_weewx_plugin.latitude and " + "aprsd_weewx_plugin.longitude to start reporting as an " + "aprs weather station.", ) return threads @@ -90,89 +97,87 @@ class WeewxMQTTPlugin(plugin.APRSDRegexCommandPluginBase): packet.get("message_text", None) # ack = packet.get("msgNo", "0") - if self.enabled: - # see if there are any weather messages in the queue. - msg = None - LOG.info("Looking for a message") - if not self.queue.empty(): - msg = self.queue.get(timeout=1) - else: - try: - msg = self.queue.get(timeout=30) - except Exception: - return "No Weewx Data" - - if not msg: - return "No Weewx data" - else: - LOG.info(f"Got a message {msg}") - # Wants format of 71.5F/54.0F Wind 1@49G7 54% - if "outTemp_F" in msg: - temperature = "{:0.2f}F".format(float(msg["outTemp_F"])) - dewpoint = "{:0.2f}F".format(float(msg["dewpoint_F"])) - else: - temperature = "{:0.2f}C".format(float(msg["outTemp_C"])) - dewpoint = "{:0.2f}C".format(float(msg["dewpoint_C"])) - - wind_direction = "{:0.0f}".format(float(msg.get("windDir", 0))) - LOG.info(f"wind direction {wind_direction}") - if "windSpeed_mps" in msg: - wind_speed = "{:0.0f}".format(float(msg["windSpeed_mps"])) - wind_gust = "{:0.0f}".format(float(msg["windGust_mps"])) - else: - wind_speed = "{:0.0f}".format(float(msg["windSpeed_mph"])) - wind_gust = "{:0.0f}".format(float(msg["windGust_mph"])) - - wind = "{}@{}G{}".format( - wind_speed, - wind_direction, - wind_gust, - ) - - humidity = "{:0.0f}%".format(float(msg["outHumidity"])) - - ts = int("{:0.0f}".format(float(msg["dateTime"]))) - ts = datetime.datetime.fromtimestamp(ts) - - # do rain in format of last hour/day/month/year - - rain = "RA {:.2f} {:.2f}/hr".format( - float(msg.get("dayRain_in", 0.00)), - float(msg.get("rainRate_inch_per_hour", 0.00)), - ) - - wx = "WX: {}/{} Wind {} {} {} {:.2f}inHg".format( - temperature, - dewpoint, - wind, - humidity, - rain, - float(msg.get("pressure_inHg", 0.00)), - ) - LOG.debug( - "Got weather {} -- len {}".format( - wx, - len(wx), - ), - ) - return wx - + # see if there are any weather messages in the queue. + msg = None + LOG.info("Looking for a message") + if not self.queue.empty(): + msg = self.queue.get(timeout=1) else: - return "WeewxMQTT Not enabled" + try: + msg = self.queue.get(timeout=30) + except Exception: + return "No Weewx Data" + + if not msg: + return "No Weewx data" + else: + LOG.info(f"Got a message {msg}") + # Wants format of 71.5F/54.0F Wind 1@49G7 54% + if "outTemp_F" in msg: + temperature = "{:0.2f}F".format(float(msg["outTemp_F"])) + dewpoint = "{:0.2f}F".format(float(msg["dewpoint_F"])) + else: + temperature = "{:0.2f}C".format(float(msg["outTemp_C"])) + dewpoint = "{:0.2f}C".format(float(msg["dewpoint_C"])) + + wind_direction = "{:0.0f}".format(float(msg.get("windDir", 0))) + LOG.info(f"wind direction {wind_direction}") + if "windSpeed_mps" in msg: + wind_speed = "{:0.0f}".format(float(msg["windSpeed_mps"])) + wind_gust = "{:0.0f}".format(float(msg["windGust_mps"])) + else: + wind_speed = "{:0.0f}".format(float(msg["windSpeed_mph"])) + wind_gust = "{:0.0f}".format(float(msg["windGust_mph"])) + + wind = "{}@{}G{}".format( + wind_speed, + wind_direction, + wind_gust, + ) + + humidity = "{:0.0f}%".format(float(msg["outHumidity"])) + + ts = int("{:0.0f}".format(float(msg["dateTime"]))) + ts = datetime.datetime.fromtimestamp(ts) + + # do rain in format of last hour/day/month/year + + rain = "RA {:.2f} {:.2f}/hr".format( + float(msg.get("dayRain_in", 0.00)), + float(msg.get("rainRate_inch_per_hour", 0.00)), + ) + + wx = "WX: {}/{} Wind {} {} {} {:.2f}inHg".format( + temperature, + dewpoint, + wind, + humidity, + rain, + float(msg.get("pressure_inHg", 0.00)), + ) + LOG.debug( + "Got weather {} -- len {}".format( + wx, + len(wx), + ), + ) + return wx class WeewxMQTTThread(threads.APRSDThread): - def __init__(self, config, wx_queue, msg_queue): + _mqtt_host = None + _mqtt_port = None + client = None + def __init__(self, wx_queue, msg_queue): super().__init__("WeewxMQTTThread") - self.config = config self.msg_queue = msg_queue self.wx_queue = wx_queue self.setup() def setup(self): LOG.info("Creating mqtt client") - self._mqtt_host = self.config["services"]["weewx"]["mqtt"]["host"] - self._mqtt_port = self.config["services"]["weewx"]["mqtt"]["port"] + self._mqtt_host = CONF.aprsd_weewx_plugin.mqtt_host + self._mqtt_port = CONF.aprsd_weewx_plugin.mqtt_port LOG.info( "Connecting to mqtt {}:{}".format( self._mqtt_host, @@ -183,9 +188,9 @@ class WeewxMQTTThread(threads.APRSDThread): self.client.on_connect = self.on_connect self.client.on_message = self.on_message self.client.connect(self._mqtt_host, self._mqtt_port, 60) - if self.config.exists("services.weewx.mqtt.user"): - username = self.config.get("services.weewx.mqtt.user") - password = self.config.get("services.weewx.mqtt.password") + if CONF.aprsd_weewx_plugin.mqtt_user: + username = CONF.aprsd_weewx_plugin.mqtt_user + password = CONF.aprsd_weewx_plugin.mqtt_password LOG.info(f"Using MQTT username/password {username}/XXXXXX") self.client.username_pw_set( username=username, @@ -224,13 +229,13 @@ class WeewxMQTTThread(threads.APRSDThread): class WeewxWXAPRSThread(threads.APRSDThread): - def __init__(self, config, wx_queue): + def __init__(self, wx_queue): super().__init__(self.__class__.__name__) - self.config = config self.wx_queue = wx_queue - self.latitude = self.config.get("services.weewx.location.latitude") - self.longitude = self.config.get("services.weewx.location.longitude") - self.callsign = self.config.get("aprsd.callsign") + self.latitude = CONF.aprsd_weewx_plugin.latitude + self.longitude = CONF.aprsd_weewx_plugin.longitude + self.callsign = CONF.callsign + self.report_interval = CONF.aprsd_weewx_plugin.report_interval self.last_send = datetime.datetime.now() if self.latitude and self.longitude: @@ -332,7 +337,7 @@ class WeewxWXAPRSThread(threads.APRSDThread): def loop(self): now = datetime.datetime.now() delta = now - self.last_send - max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} + max_timeout = {"seconds": self.report_interval} max_delta = datetime.timedelta(**max_timeout) if delta >= max_delta: if not self.wx_queue.empty(): diff --git a/requirements.txt b/requirements.txt index 4e3e0a3..c38f692 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pbr -aprsd>=2.4.0 +aprsd>=3.0.0 paho-mqtt +oslo-config diff --git a/setup.cfg b/setup.cfg index fcd901b..81803a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,10 @@ description_file = README.rst summary = HAM Radio APRSD that reports weather from a weewx weather station. +[options.entry_points] +oslo.config.opts = + aprsd_weewx_plugin.conf = aprsd_weewx_plugin.conf.opts:list_opts + [global] setup-hooks = pbr.hooks.setup_hook