Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
Hemna | 872ffd6ce4 | |
Hemna | e59fa28cb1 | |
Hemna | e4904f6d56 | |
Hemna | 6c1ae8202d | |
Hemna | 1da5037f49 | |
Hemna | 39a290383a | |
Hemna | 9bf8321d15 | |
Hemna | 514612fa05 | |
Hemna | bb7a5ed08e | |
Hemna | a5ef1f10d4 | |
Hemna | d7260fbd71 | |
Hemna | 618fb85358 | |
Hemna | eb0e5f55be | |
Hemna | 87f6dfb654 | |
Hemna | e72346c121 | |
Walter A. Boring IV | 764535a016 |
|
@ -0,0 +1,12 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: wb4bor
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
1
AUTHORS
1
AUTHORS
|
@ -1 +1,2 @@
|
||||||
Hemna <waboring@hemna.com>
|
Hemna <waboring@hemna.com>
|
||||||
|
Walter A. Boring IV <waboring@hemna.com>
|
||||||
|
|
32
ChangeLog
32
ChangeLog
|
@ -1,9 +1,39 @@
|
||||||
CHANGES
|
CHANGES
|
||||||
=======
|
=======
|
||||||
|
|
||||||
* Updated from first repo
|
v0.3.2
|
||||||
|
------
|
||||||
|
|
||||||
|
* another try
|
||||||
|
|
||||||
|
v0.3.1
|
||||||
|
------
|
||||||
|
|
||||||
|
* Updated pressure \* 10
|
||||||
|
|
||||||
|
v0.3.0
|
||||||
|
------
|
||||||
|
|
||||||
|
* Take the pressure from pressure\_inHg
|
||||||
|
|
||||||
|
v0.2.0
|
||||||
|
------
|
||||||
|
|
||||||
|
* update for 0.2.0
|
||||||
|
* Fixed pep8 failures
|
||||||
|
* Update to aprsd 3.0.0 and include config options!
|
||||||
|
* don't dump the whole packet
|
||||||
|
* use Tx to send
|
||||||
|
* Working with pre 2.7.0
|
||||||
|
* Removed trace
|
||||||
|
* lint
|
||||||
|
* Added pbr version
|
||||||
|
* Fixed missing entry in requirements.txt
|
||||||
|
* Create FUNDING.yml
|
||||||
|
|
||||||
v0.1.2
|
v0.1.2
|
||||||
------
|
------
|
||||||
|
|
||||||
|
* Fixed README.rst formatting
|
||||||
|
* Updated from first repo
|
||||||
* Initial commit
|
* Initial commit
|
||||||
|
|
6
Makefile
6
Makefile
|
@ -21,7 +21,7 @@ docs: build
|
||||||
cp Changelog docs/changelog.rst
|
cp Changelog docs/changelog.rst
|
||||||
tox -edocs
|
tox -edocs
|
||||||
|
|
||||||
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
|
clean: clean-build clean-pyc clean-test clean-dev ## remove all build, test, coverage and Python artifacts
|
||||||
|
|
||||||
clean-build: ## remove build artifacts
|
clean-build: ## remove build artifacts
|
||||||
rm -fr build/
|
rm -fr build/
|
||||||
|
@ -42,6 +42,10 @@ clean-test: ## remove test and coverage artifacts
|
||||||
rm -fr htmlcov/
|
rm -fr htmlcov/
|
||||||
rm -fr .pytest_cache
|
rm -fr .pytest_cache
|
||||||
|
|
||||||
|
clean-dev: ## Clean out the venv
|
||||||
|
rm -rf $(VENVDIR)
|
||||||
|
rm Makefile.venv
|
||||||
|
|
||||||
coverage: ## check code coverage quickly with the default Python
|
coverage: ## check code coverage quickly with the default Python
|
||||||
coverage run --source aprsd_weewx_plugin setup.py test
|
coverage run --source aprsd_weewx_plugin setup.py test
|
||||||
coverage report -m
|
coverage report -m
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import pbr.version
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = pbr.version.VersionInfo("aprsd_weewx_plugin").version_string()
|
|
@ -1,161 +0,0 @@
|
||||||
"""Main module."""
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import queue
|
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
|
||||||
from aprsd import plugin, threads, trace, utils
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
mqtt_queue = queue.Queue(maxsize=20)
|
|
||||||
|
|
||||||
|
|
||||||
class WeewxMQTTPlugin(plugin.APRSDRegexCommandPluginBase):
|
|
||||||
"""Weather
|
|
||||||
|
|
||||||
Syntax of request
|
|
||||||
|
|
||||||
weather
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
version = "1.0"
|
|
||||||
command_regex = "^[wW]"
|
|
||||||
command_name = "weather"
|
|
||||||
|
|
||||||
enabled = False
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
"""Ensure that the plugin has been configured."""
|
|
||||||
try:
|
|
||||||
LOG.info("Looking for weewx.mqtt.host config entry")
|
|
||||||
utils.check_config_option(self.config, ["services", "weewx", "mqtt", "host"])
|
|
||||||
self.enabled = True
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.error(f"Failed to find config weewx:mqtt:host {ex}")
|
|
||||||
LOG.info("Disabling the weewx mqtt subsription thread.")
|
|
||||||
self.enabled = False
|
|
||||||
|
|
||||||
def create_threads(self):
|
|
||||||
if self.enabled:
|
|
||||||
LOG.info("Creating WeewxMQTTThread")
|
|
||||||
return WeewxMQTTThread(
|
|
||||||
msg_queues=mqtt_queue,
|
|
||||||
config=self.config,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
LOG.info("WeewxMQTTPlugin not enabled due to missing config.")
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def process(self, packet):
|
|
||||||
LOG.info("WeewxMQTT Plugin")
|
|
||||||
packet.get("from")
|
|
||||||
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 mqtt_queue.empty():
|
|
||||||
msg = mqtt_queue.get(timeout=1)
|
|
||||||
else:
|
|
||||||
msg = mqtt_queue.get(timeout=30)
|
|
||||||
|
|
||||||
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["windDir"]))
|
|
||||||
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)
|
|
||||||
|
|
||||||
wx = "{} {}/{} Wind {} {}".format(
|
|
||||||
ts,
|
|
||||||
temperature,
|
|
||||||
dewpoint,
|
|
||||||
wind,
|
|
||||||
humidity,
|
|
||||||
)
|
|
||||||
LOG.debug(
|
|
||||||
"Got weather {} -- len {}".format(
|
|
||||||
wx,
|
|
||||||
len(wx),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return wx
|
|
||||||
|
|
||||||
else:
|
|
||||||
return "WeewxMQTT Not enabled"
|
|
||||||
|
|
||||||
|
|
||||||
class WeewxMQTTThread(threads.APRSDThread):
|
|
||||||
def __init__(self, msg_queues, config):
|
|
||||||
super().__init__("WeewxMQTTThread")
|
|
||||||
self.msg_queues = msg_queues
|
|
||||||
self.config = config
|
|
||||||
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_user = self.config["services"]["weewx"]["mqtt"]["user"]
|
|
||||||
self._mqtt_pass = self.config["services"]["weewx"]["mqtt"]["password"]
|
|
||||||
LOG.info(
|
|
||||||
"Connecting to mqtt {}:XXXX@{}:{}".format(
|
|
||||||
self._mqtt_user,
|
|
||||||
self._mqtt_host,
|
|
||||||
self._mqtt_port,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.client = mqtt.Client(client_id="WeewxMQTTPlugin")
|
|
||||||
self.client.on_connect = self.on_connect
|
|
||||||
self.client.on_message = self.on_message
|
|
||||||
self.client.connect(self._mqtt_host, self._mqtt_port, 60)
|
|
||||||
self.client.username_pw_set(username="hemna", password="ass")
|
|
||||||
|
|
||||||
def on_connect(self, client, userdata, flags, rc):
|
|
||||||
LOG.info(f"Connected to MQTT {self._mqtt_host} ({rc})")
|
|
||||||
client.subscribe("weather/loop")
|
|
||||||
|
|
||||||
def on_message(self, client, userdata, msg):
|
|
||||||
wx_data = json.loads(msg.payload)
|
|
||||||
LOG.debug(f"Got WX data {wx_data}")
|
|
||||||
mqtt_queue.put(wx_data)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
LOG.info("calling disconnect")
|
|
||||||
self.thread_stop = True
|
|
||||||
self.client.disconnect()
|
|
||||||
|
|
||||||
def loop(self):
|
|
||||||
LOG.info("Looping bitch")
|
|
||||||
self.client.loop_forever()
|
|
||||||
return True
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from aprsd_weewx_plugin.conf import weewx
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
weewx.register_opts(CONF)
|
|
@ -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)
|
|
@ -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,
|
||||||
|
}
|
|
@ -0,0 +1,369 @@
|
||||||
|
"""Main module."""
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import queue
|
||||||
|
import time
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
class ClearableQueue(queue.Queue):
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
self.get_nowait()
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WeewxMQTTPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
|
"""Weather
|
||||||
|
|
||||||
|
Syntax of request
|
||||||
|
|
||||||
|
weather
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[wW]"
|
||||||
|
command_name = "weather"
|
||||||
|
|
||||||
|
enabled = False
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""Ensure that the plugin has been configured."""
|
||||||
|
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(
|
||||||
|
wx_queue=self.wx_queue,
|
||||||
|
msg_queue=self.queue,
|
||||||
|
)
|
||||||
|
threads = [mqtt_thread]
|
||||||
|
|
||||||
|
# if we have position and a callsign to report
|
||||||
|
# Then we can periodically report weather data
|
||||||
|
# to APRS
|
||||||
|
if (
|
||||||
|
CONF.aprsd_weewx_plugin.latitude and
|
||||||
|
CONF.aprsd_weewx_plugin.longitude
|
||||||
|
):
|
||||||
|
LOG.info("Creating WeewxWXAPRSThread")
|
||||||
|
wx_thread = WeewxWXAPRSThread(
|
||||||
|
wx_queue=self.wx_queue,
|
||||||
|
)
|
||||||
|
threads.append(wx_thread)
|
||||||
|
else:
|
||||||
|
LOG.info(
|
||||||
|
"NOT starting Weewx WX APRS Thread due to missing "
|
||||||
|
"GPS location settings. Please set "
|
||||||
|
"aprsd_weewx_plugin.latitude and "
|
||||||
|
"aprsd_weewx_plugin.longitude to start reporting as an "
|
||||||
|
"aprs weather station.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return threads
|
||||||
|
else:
|
||||||
|
LOG.info("WeewxMQTTPlugin not enabled due to missing config.")
|
||||||
|
|
||||||
|
def process(self, packet):
|
||||||
|
LOG.info("WeewxMQTT Plugin")
|
||||||
|
packet.get("from")
|
||||||
|
packet.get("message_text", None)
|
||||||
|
# ack = packet.get("msgNo", "0")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
class WeewxMQTTThread(threads.APRSDThread):
|
||||||
|
_mqtt_host = None
|
||||||
|
_mqtt_port = None
|
||||||
|
client = None
|
||||||
|
|
||||||
|
def __init__(self, wx_queue, msg_queue):
|
||||||
|
super().__init__("WeewxMQTTThread")
|
||||||
|
self.msg_queue = msg_queue
|
||||||
|
self.wx_queue = wx_queue
|
||||||
|
self.setup()
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
LOG.info("Creating mqtt client")
|
||||||
|
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,
|
||||||
|
self._mqtt_port,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.client = mqtt.Client(client_id="WeewxMQTTPlugin")
|
||||||
|
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 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,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
LOG.info("Not using MQTT username/password")
|
||||||
|
|
||||||
|
def on_connect(self, client, userdata, flags, rc):
|
||||||
|
LOG.info(f"Connected to MQTT {self._mqtt_host} ({rc})")
|
||||||
|
client.subscribe("weather/loop")
|
||||||
|
|
||||||
|
def on_message(self, client, userdata, msg):
|
||||||
|
wx_data = json.loads(msg.payload)
|
||||||
|
LOG.debug("Got WX data")
|
||||||
|
# Make sure we have only 1 item in the queue
|
||||||
|
if self.msg_queue.qsize() >= 1:
|
||||||
|
self.msg_queue.clear()
|
||||||
|
self.msg_queue.put(wx_data)
|
||||||
|
self.wx_queue.clear()
|
||||||
|
self.wx_queue.put(wx_data)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
LOG.info(__class__.__name__+" Stop")
|
||||||
|
self.thread_stop = True
|
||||||
|
LOG.info("Stopping loop")
|
||||||
|
self.client.loop_stop()
|
||||||
|
LOG.info("Disconnecting from MQTT")
|
||||||
|
self.client.disconnect()
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
LOG.info("Loop")
|
||||||
|
self.client.loop_forever()
|
||||||
|
# self.client.loop(timeout=10, max_packets=10)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class WeewxWXAPRSThread(threads.APRSDThread):
|
||||||
|
def __init__(self, wx_queue):
|
||||||
|
super().__init__(self.__class__.__name__)
|
||||||
|
self.wx_queue = wx_queue
|
||||||
|
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:
|
||||||
|
self.position = self.get_latlon(
|
||||||
|
float(self.latitude),
|
||||||
|
float(self.longitude),
|
||||||
|
)
|
||||||
|
|
||||||
|
def decdeg2dmm_m(self, degrees_decimal):
|
||||||
|
is_positive = degrees_decimal >= 0
|
||||||
|
degrees_decimal = abs(degrees_decimal)
|
||||||
|
minutes, seconds = divmod(degrees_decimal * 3600, 60)
|
||||||
|
degrees, minutes = divmod(minutes, 60)
|
||||||
|
degrees = degrees if is_positive else -degrees
|
||||||
|
|
||||||
|
# degrees = str(int(degrees)).zfill(2).replace("-", "0")
|
||||||
|
degrees = abs(int(degrees))
|
||||||
|
# minutes = str(round(minutes + (seconds / 60), 2)).zfill(5)
|
||||||
|
minutes = int(round(minutes + (seconds / 60), 2))
|
||||||
|
hundredths = round(seconds / 60, 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"degrees": degrees, "minutes": minutes, "seconds": seconds,
|
||||||
|
"hundredths": hundredths,
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_latitude(self, degrees_decimal):
|
||||||
|
det = self.decdeg2dmm_m(degrees_decimal)
|
||||||
|
if degrees_decimal > 0:
|
||||||
|
direction = "N"
|
||||||
|
else:
|
||||||
|
direction = "S"
|
||||||
|
|
||||||
|
degrees = str(det.get("degrees")).zfill(2)
|
||||||
|
minutes = str(det.get("minutes")).zfill(2)
|
||||||
|
det.get("seconds")
|
||||||
|
hundredths = str(det.get("hundredths")).split(".")[1]
|
||||||
|
lat = f"{degrees}{str(minutes)}.{hundredths}{direction}"
|
||||||
|
return lat
|
||||||
|
|
||||||
|
def convert_longitude(self, degrees_decimal):
|
||||||
|
det = self.decdeg2dmm_m(degrees_decimal)
|
||||||
|
if degrees_decimal > 0:
|
||||||
|
direction = "E"
|
||||||
|
else:
|
||||||
|
direction = "W"
|
||||||
|
|
||||||
|
degrees = str(det.get("degrees")).zfill(3)
|
||||||
|
minutes = str(det.get("minutes")).zfill(2)
|
||||||
|
det.get("seconds")
|
||||||
|
hundredths = str(det.get("hundredths")).split(".")[1]
|
||||||
|
lon = f"{degrees}{str(minutes)}.{hundredths}{direction}"
|
||||||
|
return lon
|
||||||
|
|
||||||
|
def get_latlon(self, latitude_str, longitude_str):
|
||||||
|
return "{}/{}_".format(
|
||||||
|
self.convert_latitude(float(latitude_str)),
|
||||||
|
self.convert_longitude(float(longitude_str)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def str_or_dots(self, number, length):
|
||||||
|
# If parameter is None, fill with dots, otherwise pad with zero
|
||||||
|
# if not number:
|
||||||
|
# retn_value = "." * length
|
||||||
|
# else:
|
||||||
|
format_type = {"int": "d", "float": ".0f"}[type(number).__name__]
|
||||||
|
retn_value = "".join(("%0", str(length), format_type)) % number
|
||||||
|
|
||||||
|
return retn_value
|
||||||
|
|
||||||
|
def build_wx_packet(self, wx_data):
|
||||||
|
wind_dir = float(wx_data.get("windDir", 0.00))
|
||||||
|
wind_speed = float(wx_data.get("windSpeed_mph", 0.00))
|
||||||
|
wind_gust = float(wx_data.get("windGust_mph", 0.00))
|
||||||
|
temperature = float(wx_data.get("outTemp_F", 0.00))
|
||||||
|
rain_last_hr = float(wx_data.get("hourRain_in", 0.00))
|
||||||
|
rain_last_24_hrs = float(wx_data.get("rain24_in", 0.00))
|
||||||
|
rain_since_midnight = float(wx_data.get("day_Rain_in", 0.00))
|
||||||
|
humidity = float(wx_data.get("outHumidity", 0.00))
|
||||||
|
# * 330.863886667
|
||||||
|
# inHg * 33.8639 = mBar
|
||||||
|
pressure = float(wx_data.get("pressure_inHg", 0.00)) * 33.8639 * 10
|
||||||
|
return aprsd.packets.WeatherPacket(
|
||||||
|
from_call=self.callsign,
|
||||||
|
to_call="APRS",
|
||||||
|
latitude=self.convert_latitude(float(self.latitude)),
|
||||||
|
longitude=self.convert_longitude(float(self.longitude)),
|
||||||
|
wind_direction=int(wind_dir),
|
||||||
|
wind_speed=wind_speed,
|
||||||
|
wind_gust=wind_gust,
|
||||||
|
temperature=temperature,
|
||||||
|
rain_1h=rain_last_hr,
|
||||||
|
rain_24h=rain_last_24_hrs,
|
||||||
|
rain_since_midnight=rain_since_midnight,
|
||||||
|
humidity=int(round(humidity)),
|
||||||
|
pressure=pressure,
|
||||||
|
comment="APRSD WX http://pypi.org/project/aprsd",
|
||||||
|
)
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
delta = now - self.last_send
|
||||||
|
max_timeout = {"seconds": self.report_interval}
|
||||||
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
|
if delta >= max_delta:
|
||||||
|
if not self.wx_queue.empty():
|
||||||
|
wx = self.wx_queue.get(timeout=1)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
wx = self.wx_queue.get(timeout=5)
|
||||||
|
except Exception:
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not wx:
|
||||||
|
# just keep looping
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# we have Weather now, so lets format the data
|
||||||
|
# and then send it out to APRS
|
||||||
|
packet = self.build_wx_packet(wx)
|
||||||
|
packet.retry_count = 1
|
||||||
|
tx.send(packet)
|
||||||
|
self.last_send = datetime.datetime.now()
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
|
@ -1,2 +1,4 @@
|
||||||
pbr
|
pbr
|
||||||
aprsd>=2.2.0
|
aprsd>=3.0.0
|
||||||
|
paho-mqtt
|
||||||
|
oslo-config
|
||||||
|
|
|
@ -19,6 +19,10 @@ description_file =
|
||||||
README.rst
|
README.rst
|
||||||
summary = HAM Radio APRSD that reports weather from a weewx weather station.
|
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]
|
[global]
|
||||||
setup-hooks =
|
setup-hooks =
|
||||||
pbr.hooks.setup_hook
|
pbr.hooks.setup_hook
|
||||||
|
|
Loading…
Reference in New Issue