Compare commits

...

26 Commits

Author SHA1 Message Date
0a0cc61c41 Add enabled config option to aprsd_weewx_plugin block 2026-03-09 22:28:17 -04:00
db9f7f982d Remove aprsd from dependencies 2026-01-27 13:11:50 -05:00
faa328396c update docs and readme 2026-01-25 21:50:59 -05:00
a3f5510bfb added config export pattern 2026-01-23 12:13:06 -05:00
fa012069c1 Added enabled config option for the plugin. 2026-01-22 17:48:44 -05:00
93095879b9 switch project to pyproject.toml and update tox.ini 2026-01-16 13:36:26 -05:00
c71a7d3598
Update comment to include PyPI link 2025-12-03 21:10:08 -05:00
6cbafdd1d2
Update comment in weewx.py 2025-12-03 21:04:02 -05:00
153e2105b2
Update APRSD WX comment URL to a new link 2025-12-03 21:01:25 -05:00
8e75f96c03 update for aprsd master 2025-06-17 21:45:49 -04:00
872ffd6ce4 don't dump the packet when you get it from weewx 2023-10-06 16:28:56 -04:00
e59fa28cb1 Update building WeatherPacket 2023-10-06 16:26:51 -04:00
e4904f6d56 another try 2023-04-20 16:14:57 -04:00
6c1ae8202d Updated pressure * 10 2023-04-20 15:58:47 -04:00
1da5037f49 Take the pressure from pressure_inHg 2023-04-20 15:43:46 -04:00
39a290383a update for 0.2.0 2023-01-09 20:44:55 -05:00
9bf8321d15 Fixed pep8 failures 2023-01-09 20:41:16 -05:00
514612fa05 Update to aprsd 3.0.0 and include config options!
This patch adds the entry points for aprsd to generate
config options when this plugin is installed.

The patch also switches over to using the new oslo_config style
config object.
2022-12-29 12:18:16 -05:00
bb7a5ed08e don't dump the whole packet 2022-12-22 08:24:21 -05:00
a5ef1f10d4 use Tx to send 2022-12-22 08:21:33 -05:00
d7260fbd71 Working with pre 2.7.0 2022-12-20 12:23:31 -05:00
618fb85358 Removed trace 2022-12-20 12:23:31 -05:00
eb0e5f55be lint 2022-12-20 12:23:31 -05:00
87f6dfb654 Added pbr version 2022-12-20 12:23:31 -05:00
e72346c121 Fixed missing entry in requirements.txt 2022-12-20 12:23:31 -05:00
764535a016
Create FUNDING.yml 2021-12-14 11:15:11 -05:00
33 changed files with 1531 additions and 383 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -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']

View File

@ -1,23 +1,29 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-json
- id: detect-private-key
- id: check-merge-conflict
- id: check-case-conflict
- id: check-docstring-first
- id: check-builtin-literals
- id: check-illegal-windows-names
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.16.0
rev: v2.7.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/dizballanze/gray
rev: v0.10.1
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.10
hooks:
- id: gray
###### Relevant part below ######
- id: ruff-check
types_or: [python, pyi]
args: ["check", "--select", "I", "--fix"]
###### Relevant part above ######
- id: ruff-format
types_or: [python, pyi]

View File

@ -1 +1,2 @@
Hemna <waboring@hemna.com>
Walter A. Boring IV <waboring@hemna.com>

View File

@ -1,9 +1,39 @@
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
------
* Fixed README.rst formatting
* Updated from first repo
* Initial commit

View File

@ -17,11 +17,11 @@ help: # Help for the Makefile
dev: venv ## Create the virtualenv with all the requirements installed
docs: build
cp README.rst docs/readme.rst
cp README.md docs/readme.rst
cp Changelog docs/changelog.rst
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
rm -fr build/
@ -42,6 +42,10 @@ clean-test: ## remove test and coverage artifacts
rm -fr htmlcov/
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 run --source aprsd_weewx_plugin setup.py test
coverage report -m

139
README.md Normal file
View File

@ -0,0 +1,139 @@
# APRSD Weewx Plugin
[![PyPI](https://img.shields.io/pypi/v/aprsd-weewx-plugin.svg)](https://pypi.org/project/aprsd-weewx-plugin/)
[![Status](https://img.shields.io/pypi/status/aprsd-weewx-plugin.svg)](https://pypi.org/project/aprsd-weewx-plugin/)
[![Python Version](https://img.shields.io/pypi/pyversions/aprsd-weewx-plugin)](https://pypi.org/project/aprsd-weewx-plugin)
[![License](https://img.shields.io/pypi/l/aprsd-weewx-plugin)](https://opensource.org/licenses/GNU%20GPL%20v3.0)
[![Read the Docs](https://img.shields.io/readthedocs/aprsd-weewx-plugin/latest.svg?label=Read%20the%20Docs)](https://aprsd-weewx-plugin.readthedocs.io/)
[![Tests](https://github.com/hemna/aprsd-weewx-plugin/workflows/Tests/badge.svg)](https://github.com/hemna/aprsd-weewx-plugin/actions?workflow=Tests)
[![Codecov](https://codecov.io/gh/hemna/aprsd-weewx-plugin/branch/main/graph/badge.svg)](https://codecov.io/gh/hemna/aprsd-weewx-plugin)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
## Features
* **MQTT Integration**: Connects to Weewx weather station via MQTT to receive real-time weather data
* **APRS Weather Queries**: Responds to APRS messages with current weather conditions
* **Automatic Weather Reporting**: Optionally reports weather data to APRS-IS at regular intervals
* **Comprehensive Weather Data**: Includes temperature, dewpoint, wind speed/direction, humidity, pressure, and rainfall
* **Flexible Units**: Supports both imperial (Fahrenheit, mph, inHg) and metric (Celsius, m/s, mBar) units
## Requirements
* **APRSD**: Version 4.2.0 or higher
* **Weewx**: Weather station software configured to publish MQTT messages
* **MQTT Broker**: Accessible MQTT server (e.g., Mosquitto, Eclipse Mosquitto)
* **Python**: 3.8 or higher
## Installation
You can install **APRSD Weewx Plugin** via [pip](https://pip.pypa.io/) from [PyPI](https://pypi.org/):
```console
$ pip install aprsd-weewx-plugin
```
## Configuration
### Basic Configuration
Add the plugin to your APRSD configuration file (typically `aprsd.yml`):
```yaml
aprsd:
enabled_plugins:
- aprsd_weewx_plugin.weewx.WeewxMQTTPlugin
aprsd_weewx_plugin:
enabled: true
mqtt_host: localhost
mqtt_port: 1883
mqtt_user: weewx
mqtt_password: your_password_here
```
### Automatic Weather Reporting
To enable automatic weather reporting to APRS-IS, add latitude and longitude:
```yaml
aprsd_weewx_plugin:
enabled: true
mqtt_host: localhost
mqtt_port: 1883
latitude: 37.7749
longitude: -122.4194
report_interval: 300 # Report every 5 minutes (in seconds)
```
### Weewx MQTT Configuration
Ensure your Weewx installation is configured to publish weather data to MQTT. Add this to your Weewx configuration:
```ini
[MQTT]
host = localhost
port = 1883
topic = weather/loop
unit_system = US
```
## Usage
### Querying Weather via APRS
Once configured, you can query weather data by sending an APRS message to your station's callsign with a message starting with `w` or `W`:
**Example APRS Interaction:**
```text
You: WB4BOR-1>APRS,TCPIP*:>w WB4BOR
WB4BOR: WX: 72.5F/54.0F Wind 5@270G12 65% RA 0.00 0.00/hr 29.92inHg
```
**Response Format:**
```text
WX: <temp>/<dewpoint> Wind <speed>@<direction>G<gust> <humidity>% RA <day_rain> <rate>/hr <pressure>inHg
```
**Example Response Breakdown:**
* `72.5F/54.0F` - Temperature 72.5°F, Dewpoint 54.0°F
* `Wind 5@270G12` - Wind speed 5 mph from 270° (west) with gusts to 12 mph
* `65%` - Relative humidity
* `RA 0.00 0.00/hr` - Daily rainfall 0.00 inches, current rate 0.00 inches/hour
* `29.92inHg` - Barometric pressure
### Automatic Weather Reporting
When latitude and longitude are configured, the plugin automatically sends weather packets to APRS-IS at the configured interval. These packets appear on APRS.fi and other APRS services.
### Exporting Configuration
You can export the plugin's configuration options using the CLI tool:
```console
$ aprsd-weewx-plugin-export-config
```
This will output all available configuration options in JSON format.
## Contributing
Contributions are very welcome.
To learn more, see the [Contributor Guide](contributing).
## License
Distributed under the terms of the [GNU GPL v3.0 license](https://opensource.org/licenses/GNU%20GPL%20v3.0),
**APRSD Weewx Plugin** is free and open source software.
## Issues
If you encounter any problems,
please [file an issue](https://github.com/hemna/aprsd-weewx-plugin/issues) along with a detailed description.
## Credits
This project was generated from [@hemna](https://github.com/hemna)'s [APRSD Plugin Python Cookiecutter](https://github.com/hemna/cookiecutter-aprsd-plugin) template.

View File

@ -1,99 +0,0 @@
APRSD Weewx Plugin
===================
|PyPI| |Status| |Python Version| |License|
|Read the Docs| |Tests| |Codecov|
|pre-commit|
.. |PyPI| image:: https://img.shields.io/pypi/v/aprsd-weewx-plugin.svg
:target: https://pypi.org/project/aprsd-weewx-plugin/
:alt: PyPI
.. |Status| image:: https://img.shields.io/pypi/status/aprsd-weewx-plugin.svg
:target: https://pypi.org/project/aprsd-weewx-plugin/
:alt: Status
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/aprsd-weewx-plugin
:target: https://pypi.org/project/aprsd-weewx-plugin
:alt: Python Version
.. |License| image:: https://img.shields.io/pypi/l/aprsd-weewx-plugin
:target: https://opensource.org/licenses/GNU GPL v3.0
:alt: License
.. |Read the Docs| image:: https://img.shields.io/readthedocs/aprsd-weewx-plugin/latest.svg?label=Read%20the%20Docs
:target: https://aprsd-weewx-plugin.readthedocs.io/
:alt: Read the documentation at https://aprsd-weewx-plugin.readthedocs.io/
.. |Tests| image:: https://github.com/hemna/aprsd-weewx-plugin/workflows/Tests/badge.svg
:target: https://github.com/hemna/aprsd-weewx-plugin/actions?workflow=Tests
:alt: Tests
.. |Codecov| image:: https://codecov.io/gh/hemna/aprsd-weewx-plugin/branch/main/graph/badge.svg
:target: https://codecov.io/gh/hemna/aprsd-weewx-plugin
:alt: Codecov
.. |pre-commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white
:target: https://github.com/pre-commit/pre-commit
:alt: pre-commit
Features
--------
* TODO
Requirements
------------
* TODO
Installation
------------
You can install *APRSD Weewx Plugin* via pip_ from PyPI_:
.. code:: console
$ pip install aprsd-weewx-plugin
Usage
-----
Please see the `Command-line Reference <Usage_>`_ for details.
Contributing
------------
Contributions are very welcome.
To learn more, see the `Contributor Guide`_.
License
-------
Distributed under the terms of the `GNU GPL v3.0 license`_,
*APRSD Weewx Plugin* is free and open source software.
Issues
------
If you encounter any problems,
please `file an issue`_ along with a detailed description.
Credits
-------
This project was generated from `@hemna`_'s `APRSD Plugin Python Cookiecutter`_ template.
.. _@hemna: https://github.com/hemna
.. _Cookiecutter: https://github.com/audreyr/cookiecutter
.. _GNU GPL v3.0 license: https://opensource.org/licenses/GNU GPL v3.0
.. _PyPI: https://pypi.org/
.. _APRSD Plugin Python Cookiecutter: https://github.com/hemna/cookiecutter-aprsd-plugin
.. _file an issue: https://github.com/hemna/aprsd-weewx-plugin/issues
.. _pip: https://pip.pypa.io/
.. github-only
.. _Contributor Guide: CONTRIBUTING.rst
.. _Usage: https://aprsd-weewx-plugin.readthedocs.io/en/latest/usage.html

View File

@ -0,0 +1,17 @@
# 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.
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("aprsd_weewx_plugin")
except PackageNotFoundError:
pass

View File

@ -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

50
aprsd_weewx_plugin/cli.py Normal file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""
CLI tool for aprsd-weewx-plugin configuration export.
"""
import json
import sys
def export_config_cmd(format="json"):
"""Export plugin configuration options."""
try:
from aprsd_weewx_plugin.conf.opts import export_config
result = export_config(format=format)
if format == "json":
print(result)
else:
print(json.dumps(result, indent=2))
return 0
except ImportError as e:
print(f"Error: {e}", file=sys.stderr)
print("\nTo export config, install oslo.config:", file=sys.stderr)
print(" pip install oslo.config", file=sys.stderr)
return 1
except Exception as e:
print(f"Error exporting config: {e}", file=sys.stderr)
return 1
def main():
"""Main entry point for CLI."""
import argparse
parser = argparse.ArgumentParser(description="Export aprsd-weewx-plugin configuration options")
parser.add_argument(
"--format",
choices=["dict", "json"],
default="json",
help="Output format (default: json)",
)
args = parser.parse_args()
sys.exit(export_config_cmd(format=args.format))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,6 @@
from oslo_config import cfg
from aprsd_weewx_plugin.conf import main
CONF = cfg.CONF
main.register_opts(CONF)

View File

@ -0,0 +1,63 @@
from oslo_config import cfg
weewx_group = cfg.OptGroup(
name="aprsd_weewx_plugin",
title="APRSD Weewx Plugin settings",
)
weewx_opts = [
cfg.BoolOpt(
"enabled",
default=True,
help="Enable the weewx plugin",
),
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,
}

View File

@ -0,0 +1,146 @@
# 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 importlib.util
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)
def export_config(format="dict"):
"""
Export configuration options as a simple data structure.
This function extracts configuration information from oslo_config
option objects and returns it in a simple dict or JSON format.
Works independently of aprsd installation.
Args:
format: Output format - 'dict' (default) or 'json'
Returns:
dict or JSON string containing all configuration options with:
- name: option name
- type: option type (StrOpt, BoolOpt, IntOpt, etc.)
- default: default value
- help: help text
- required: whether the option is required
- choices: list of valid choices (if applicable)
- secret: whether the option contains secret data (if applicable)
- min/max: min/max values for numeric types (if applicable)
Raises:
ImportError: if oslo_config is not installed
"""
# Check if oslo_config is available
if importlib.util.find_spec("oslo_config") is None:
raise ImportError(
"oslo_config is required to export configuration. "
"Install it with: pip install oslo.config"
)
opts = list_opts()
result = {}
for group_name, opt_list in opts:
result[group_name] = []
for opt in opt_list:
opt_dict = {
"name": opt.name,
"type": type(opt).__name__,
"default": getattr(opt, "default", None),
"help": getattr(opt, "help", ""),
"required": not hasattr(opt, "default") or getattr(opt, "default", None) is None,
}
# Add additional attributes if available
if hasattr(opt, "choices") and opt.choices:
opt_dict["choices"] = list(opt.choices)
if hasattr(opt, "secret") and opt.secret:
opt_dict["secret"] = True
if hasattr(opt, "min") and opt.min is not None:
opt_dict["min"] = opt.min
if hasattr(opt, "max") and opt.max is not None:
opt_dict["max"] = opt.max
result[group_name].append(opt_dict)
if format == "json":
import json
return json.dumps(result, indent=2)
return result

View File

@ -0,0 +1,63 @@
from oslo_config import cfg
weewx_group = cfg.OptGroup(
name="aprsd_weewx_plugin",
title="APRSD Weewx Plugin settings",
)
weewx_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable the plugin?",
),
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,
}

371
aprsd_weewx_plugin/weewx.py Normal file
View File

@ -0,0 +1,371 @@
"""Main module."""
import datetime
import json
import logging
import queue
import time
import paho.mqtt.client as mqtt
from aprsd import plugin, threads
from aprsd.packets import core
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 = CONF.aprsd_weewx_plugin.enabled
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_disconnect = self.on_disconnect
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_disconnect(self, client, userdata, rc):
LOG.info(f"Disconnected from MQTT {self._mqtt_host} ({rc})")
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 core.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

View File

@ -0,0 +1,37 @@
aprsd\_weewx\_plugin.conf package
=================================
Submodules
----------
aprsd\_weewx\_plugin.conf.main module
-------------------------------------
.. automodule:: aprsd_weewx_plugin.conf.main
:members:
:show-inheritance:
:undoc-members:
aprsd\_weewx\_plugin.conf.opts module
-------------------------------------
.. automodule:: aprsd_weewx_plugin.conf.opts
:members:
:show-inheritance:
:undoc-members:
aprsd\_weewx\_plugin.conf.weewx module
--------------------------------------
.. automodule:: aprsd_weewx_plugin.conf.weewx
:members:
:show-inheritance:
:undoc-members:
Module contents
---------------
.. automodule:: aprsd_weewx_plugin.conf
:members:
:show-inheritance:
:undoc-members:

View File

@ -0,0 +1,37 @@
aprsd\_weewx\_plugin package
============================
Subpackages
-----------
.. toctree::
:maxdepth: 4
aprsd_weewx_plugin.conf
Submodules
----------
aprsd\_weewx\_plugin.cli module
-------------------------------
.. automodule:: aprsd_weewx_plugin.cli
:members:
:show-inheritance:
:undoc-members:
aprsd\_weewx\_plugin.weewx module
---------------------------------
.. automodule:: aprsd_weewx_plugin.weewx
:members:
:show-inheritance:
:undoc-members:
Module contents
---------------
.. automodule:: aprsd_weewx_plugin
:members:
:show-inheritance:
:undoc-members:

7
docs/apidoc/modules.rst Normal file
View File

@ -0,0 +1,7 @@
aprsd_weewx_plugin
==================
.. toctree::
:maxdepth: 4
aprsd_weewx_plugin

View File

@ -1 +1,4 @@
.. include:: ../AUTHORS.rst
Authors
=======
.. include:: ../AUTHORS

39
docs/changelog.rst Normal file
View File

@ -0,0 +1,39 @@
CHANGES
=======
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
------
* Fixed README.rst formatting
* Updated from first repo
* Initial commit

View File

@ -20,12 +20,10 @@
import os
import sys
sys.path.insert(0, os.path.abspath(".."))
import aprsd_weewx_plugin
# -- General configuration ---------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@ -34,7 +32,7 @@ import aprsd_weewx_plugin
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"]
extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "myst_parser"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@ -42,8 +40,10 @@ templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
source_suffix = {
".rst": "restructuredtext",
".md": "markdown",
}
# The master toctree document.
master_doc = "index"
@ -67,7 +67,7 @@ release = aprsd_weewx_plugin.__version__
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@ -97,7 +97,7 @@ html_theme = "alabaster"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# html_static_path = ["_static"] # Commented out since _static directory doesn't exist
# -- Options for HTMLHelp output ---------------------------------------
@ -112,15 +112,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@ -131,9 +128,11 @@ latex_elements = {
# [howto, manual, or own class]).
latex_documents = [
(
master_doc, "aprsd_weewx_plugin.tex",
master_doc,
"aprsd_weewx_plugin.tex",
"APRSD Weewx Plugin Documentation",
"Walter A. Boring IV", "manual",
"Walter A. Boring IV",
"manual",
),
]
@ -144,9 +143,11 @@ latex_documents = [
# (source start file, name, description, authors, manual section).
man_pages = [
(
master_doc, "aprsd_weewx_plugin",
master_doc,
"aprsd_weewx_plugin",
"APRSD Weewx Plugin Documentation",
[author], 1,
[author],
1,
),
]
@ -158,7 +159,8 @@ man_pages = [
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc, "aprsd_weewx_plugin",
master_doc,
"aprsd_weewx_plugin",
"APRSD Weewx Plugin Documentation",
author,
"aprsd_weewx_plugin",

View File

@ -1 +1,4 @@
Contributing
============
.. include:: ../CONTRIBUTING.rst

View File

@ -1 +1,4 @@
.. include:: ../HISTORY.rst
History
========
.. include:: ../ChangeLog

View File

@ -5,11 +5,19 @@ Welcome to APRSD Nearest station plugin's documentation!
:maxdepth: 2
:caption: Contents:
readme
readme.md
installation
usage
contributing
authors
history
changelog
.. toctree::
:maxdepth: 4
:caption: API Documentation:
apidoc/modules
Indices and tables
==================

View File

@ -4,6 +4,15 @@
Installation
============
Prerequisites
-------------
Before installing the plugin, ensure you have:
* **APRSD** version 4.2.0 or higher installed and configured
* **Weewx** weather station software running and configured
* Access to an **MQTT broker** (local or remote)
* **Python** 3.8 or higher
Stable release
--------------
@ -44,8 +53,86 @@ Once you have a copy of the source, you can install it with:
.. code-block:: console
$ python setup.py install
$ pip install .
Configuration
-------------
After installation, you need to configure the plugin in your APRSD configuration file.
Basic Configuration
~~~~~~~~~~~~~~~~~~~~
Add the plugin to your APRSD configuration (typically ``aprsd.yml``):
.. code-block:: yaml
aprsd:
enabled_plugins:
- aprsd_weewx_plugin.weewx.WeewxMQTTPlugin
aprsd_weewx_plugin:
enabled: true
mqtt_host: localhost
mqtt_port: 1883
mqtt_user: weewx
mqtt_password: your_password_here
With Automatic Weather Reporting
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To enable automatic weather reporting to APRS-IS, add latitude and longitude:
.. code-block:: yaml
aprsd_weewx_plugin:
enabled: true
mqtt_host: localhost
mqtt_port: 1883
mqtt_user: weewx
mqtt_password: your_password_here
latitude: 37.7749
longitude: -122.4194
report_interval: 300 # Report every 5 minutes (in seconds)
Configuration Options
~~~~~~~~~~~~~~~~~~~~~
* ``enabled`` (boolean): Enable/disable the plugin (default: false)
* ``mqtt_host`` (string): MQTT broker hostname (required)
* ``mqtt_port`` (integer): MQTT broker port (required)
* ``mqtt_user`` (string): MQTT username (optional)
* ``mqtt_password`` (string): MQTT password (optional, but recommended)
* ``latitude`` (float): Station latitude for automatic reporting (optional)
* ``longitude`` (float): Station longitude for automatic reporting (optional)
* ``report_interval`` (integer): Seconds between automatic reports (default: 60)
Weewx MQTT Setup
~~~~~~~~~~~~~~~~
Ensure your Weewx installation publishes weather data to MQTT. Add this to your Weewx configuration (``weewx.conf``):
.. code-block:: ini
[MQTT]
host = localhost
port = 1883
topic = weather/loop
unit_system = US
The plugin subscribes to the ``weather/loop`` topic and expects JSON-formatted weather data.
Exporting Configuration
~~~~~~~~~~~~~~~~~~~~~~~
You can view all available configuration options using the CLI tool:
.. code-block:: console
$ aprsd-weewx-plugin-export-config
This outputs all configuration options in JSON format.
.. _Github repo: https://github.com/hemna/aprsd-weewx-plugin
.. _tarball: https://github.com/hemna/aprsd-weewx-plugin/tarball/master

50
docs/readme.md Normal file
View File

@ -0,0 +1,50 @@
# APRSD Weewx Plugin
[![PyPI](https://img.shields.io/pypi/v/aprsd-weewx-plugin.svg)](https://pypi.org/project/aprsd-weewx-plugin/)
[![Status](https://img.shields.io/pypi/status/aprsd-weewx-plugin.svg)](https://pypi.org/project/aprsd-weewx-plugin/)
[![Python Version](https://img.shields.io/pypi/pyversions/aprsd-weewx-plugin)](https://pypi.org/project/aprsd-weewx-plugin)
[![License](https://img.shields.io/pypi/l/aprsd-weewx-plugin)](https://opensource.org/licenses/GNU%20GPL%20v3.0)
[![Read the Docs](https://img.shields.io/readthedocs/aprsd-weewx-plugin/latest.svg?label=Read%20the%20Docs)](https://aprsd-weewx-plugin.readthedocs.io/)
[![Tests](https://github.com/hemna/aprsd-weewx-plugin/workflows/Tests/badge.svg)](https://github.com/hemna/aprsd-weewx-plugin/actions?workflow=Tests)
[![Codecov](https://codecov.io/gh/hemna/aprsd-weewx-plugin/branch/main/graph/badge.svg)](https://codecov.io/gh/hemna/aprsd-weewx-plugin)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
## Features
* TODO
## Requirements
* TODO
## Installation
You can install **APRSD Weewx Plugin** via [pip](https://pip.pypa.io/) from [PyPI](https://pypi.org/):
```console
$ pip install aprsd-weewx-plugin
```
## Usage
Please see the [Command-line Reference](https://aprsd-weewx-plugin.readthedocs.io/en/latest/usage.html) for details.
## Contributing
Contributions are very welcome.
To learn more, see the [Contributor Guide](contributing).
## License
Distributed under the terms of the [GNU GPL v3.0 license](https://opensource.org/licenses/GNU%20GPL%20v3.0),
**APRSD Weewx Plugin** is free and open source software.
## Issues
If you encounter any problems,
please [file an issue](https://github.com/hemna/aprsd-weewx-plugin/issues) along with a detailed description.
## Credits
This project was generated from [@hemna](https://github.com/hemna)'s [APRSD Plugin Python Cookiecutter](https://github.com/hemna/cookiecutter-aprsd-plugin) template.

145
docs/usage.rst Normal file
View File

@ -0,0 +1,145 @@
Usage
=====
The APRSD Weewx Plugin provides two main functions:
1. **Weather Query Responses**: Responds to APRS messages with current weather data
2. **Automatic Weather Reporting**: Periodically sends weather packets to APRS-IS
Querying Weather via APRS
--------------------------
Once the plugin is configured and running, you can query weather data by sending an APRS message to your station's callsign. The plugin responds to messages that start with ``w`` or ``W``.
Example Interaction
~~~~~~~~~~~~~~~~~~~
Here's a typical interaction:
**You send:**
::
WB4BOR-1>APRS,TCPIP*:>w WB4BOR
**Plugin responds:**
::
WB4BOR>APRS,TCPIP*:>WX: 72.5F/54.0F Wind 5@270G12 65% RA 0.00 0.00/hr 29.92inHg
Response Format
~~~~~~~~~~~~~~~
The weather response follows this format:
::
WX: <temp>/<dewpoint> Wind <speed>@<direction>G<gust> <humidity>% RA <day_rain> <rate>/hr <pressure>inHg
Field Breakdown
~~~~~~~~~~~~~~~
* **Temperature/Dewpoint**: Current temperature and dewpoint (Fahrenheit or Celsius)
* **Wind**: Wind speed (mph or m/s), direction in degrees, and gust speed
* **Humidity**: Relative humidity percentage
* **Rain**: Daily rainfall total and current rain rate
* **Pressure**: Barometric pressure (inHg or mBar)
Example Responses
~~~~~~~~~~~~~~~~~
**Imperial Units (Fahrenheit, mph, inHg):**
::
WX: 72.5F/54.0F Wind 5@270G12 65% RA 0.00 0.00/hr 29.92inHg
**Metric Units (Celsius, m/s, mBar):**
::
WX: 22.5C/12.2C Wind 2@270G5 65% RA 0.00 0.00/hr 1013.25mBar
**With Rain:**
::
WX: 68.0F/55.0F Wind 8@180G15 72% RA 0.25 0.10/hr 30.05inHg
Automatic Weather Reporting
---------------------------
When latitude and longitude are configured, the plugin automatically sends weather packets to APRS-IS at regular intervals. These packets:
* Appear on APRS.fi and other APRS mapping services
* Include your station's position and weather data
* Are sent at the interval specified by ``report_interval`` (default: 60 seconds)
Example APRS Weather Packet
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The plugin sends standard APRS weather packets that look like this on APRS.fi:
::
WB4BOR>APRS,TCPIP*:@251234z3745.50N/12225.00W_270/005g012t072r000p000P000h65b10130
This packet includes:
* Timestamp (25th day, 12:34 UTC)
* Position (37°45.50'N, 122°25.00'W)
* Wind direction 270° at 5 mph, gusting to 12 mph
* Temperature 72°F
* Rainfall data
* Pressure and humidity
Troubleshooting
---------------
No Response to Weather Queries
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the plugin doesn't respond to weather queries:
1. Check that the plugin is enabled in your APRSD configuration
2. Verify MQTT connection is working (check APRSD logs)
3. Ensure Weewx is publishing to the ``weather/loop`` topic
4. Check that MQTT credentials are correct
No Automatic Reports
~~~~~~~~~~~~~~~~~~~~~
If automatic weather reports aren't appearing:
1. Verify ``latitude`` and ``longitude`` are configured
2. Check that your APRSD callsign is set correctly
3. Ensure APRSD is connected to APRS-IS
4. Review the ``report_interval`` setting
MQTT Connection Issues
~~~~~~~~~~~~~~~~~~~~~~~
If you see MQTT connection errors:
1. Verify the MQTT broker is running and accessible
2. Check firewall settings for the MQTT port
3. Verify MQTT username and password (if required)
4. Test MQTT connection with a tool like ``mosquitto_sub``:
.. code-block:: console
$ mosquitto_sub -h localhost -p 1883 -t weather/loop
Testing the Plugin
------------------
You can test the plugin by:
1. **Checking APRSD logs** for plugin initialization messages
2. **Sending a test message** via APRS to your callsign with "w" or "W"
3. **Monitoring MQTT traffic** to verify Weewx is publishing data
4. **Checking APRS.fi** for automatic weather reports (if enabled)
Example Test Session
~~~~~~~~~~~~~~~~~~~~
1. Start APRSD with the plugin enabled
2. Wait for MQTT connection confirmation in logs
3. Send APRS message: ``w YOURCALL``
4. Receive weather response
5. Check APRS.fi for automatic reports (if configured)

154
pyproject.toml Normal file
View File

@ -0,0 +1,154 @@
[project]
# This is the name of your project. The first time you publish this
# package, this name will be registered for you. It will determine how
# users can install this project, e.g.:
#
# $ pip install sampleproject
#
# And where it will live on PyPI: https://pypi.org/project/sampleproject/
#
# There are some restrictions on what makes a valid project name
# specification here:
# https://packaging.python.org/specifications/core-metadata/#name
name = "aprsd-weewx-plugin"
description = "APRSD Plugin to send weather data to Weewx via MQTT."
# Specify which Python versions you support. In contrast to the
# 'Programming Language' classifiers in this file, 'pip install' will check this
# and refuse to install the project if the version does not match. See
# https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
requires-python = ">=3.8"
dynamic = ["version"]
dependencies = [
"paho-mqtt",
"oslo_config",
]
# This is an optional longer description of your project that represents
# the body of text which users will see when they visit PyPI.
#
# Often, this is the same as your README, so you can just read it in from
# that file directly.
#
# This field corresponds to the "Description" metadata field:
# https://packaging.python.org/specifications/core-metadata/#description-optional
readme = {file = "README.md", content-type = "text/markdown"}
# This is either text indicating the license for the distribution, or a file
# that contains the license.
# https://packaging.python.org/en/latest/specifications/core-metadata/#license
license = {file = "LICENSE"}
# This should be your name or the name of the organization who originally
# authored the project, and a valid email address corresponding to the name
# listed.
authors = [
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
]
# This should be your name or the names of the organization who currently
# maintains the project, and a valid email address corresponding to the name
# listed.
maintainers = [
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
]
# This field adds keywords for your project which will appear on the
# project page. What does your project relate to?
#
# Note that this is a list of additional keywords, separated
# by commas, to be used to assist searching for the distribution in a
# larger catalog.
keywords = [
"aprs",
"aprs-is",
"aprsd",
"aprsd-server",
"aprsd-client",
"aprsd-socket",
"aprsd-socket-server",
"aprsd-socket-client",
]
# Classifiers help users find your project by categorizing it.
#
# For a list of valid classifiers, see https://pypi.org/classifiers/
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Information Technology",
"Topic :: Communications :: Ham Radio",
"Topic :: Internet",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
]
# List additional groups of dependencies here (e.g. development
# dependencies). Users will be able to install these using the "extras"
# syntax, for example:
#
# $ pip install sampleproject[dev]
#
# Optional dependencies the project provides. These are commonly
# referred to as "extras". For a more extensive definition see:
# https://packaging.python.org/en/latest/specifications/dependency-specifiers/#extras
# [project.optional-dependencies]
# List URLs that are relevant to your project
#
# This field corresponds to the "Project-URL" and "Home-Page" metadata fields:
# https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use
# https://packaging.python.org/specifications/core-metadata/#home-page-optional
#
# Examples listed include a pattern for specifying where the package tracks
# issues, where the source is hosted, where to say thanks to the package
# maintainers, and where to support the project financially. The key is
# what's used to render the link text on PyPI.
[project.urls]
# "Homepage" = "https://github.com/hemna/aprsd-admin-extension"
# "Bug Reports" = "https://github.com/hemna/aprsd-admin-extension/issues"
# "Source" = "https://github.com/hemna/aprsd-admin-extension"
# Documentation = "https://aprsd-joke-plugin.readthedocs.io/en/latest/"
[project.entry-points."oslo.config.opts"]
"aprsd_weewx_plugin.conf" = "aprsd_weewx_plugin.conf.opts:list_opts"
[project.scripts]
"aprsd-weewx-plugin-export-config" = "aprsd_weewx_plugin.cli:main"
# If you are using a different build backend, you will need to change this.
[tool.setuptools]
# Packages to include
# Packages to include - use find: to auto-discover all packages and subpackages
packages = {find = {}}
package-data = {"sample" = ["*.dat"]}
[build-system]
requires = [
"setuptools>=69.5.0",
"setuptools_scm>=0",
"wheel",
]
build-backend = "setuptools.build_meta"
[tool.isort]
force_sort_within_sections = true
multi_line_output = 4
line_length = 88
# Inform isort of paths to import names that should be considered part of the "First Party" group.
#src_paths = ["src/openstack_loadtest"]
skip_gitignore = true
# If you need to skip/exclude folders, consider using skip_glob as that will allow the
# isort defaults for skip to remain without the need to duplicate them.
[tool.coverage.run]
branch = true
[tool.setuptools_scm]

View File

@ -1,12 +0,0 @@
pip
pip-tools
bump2version
wheel
watchdog
flake8
tox
coverage
Sphinx
twine
pytest==6.2.5
gray

View File

@ -1,2 +0,0 @@
pbr
aprsd>=2.2.0

View File

@ -1,40 +0,0 @@
[metadata]
name = aprsd_weewx_plugin
long_description = file: README.rst
long_description_content_type = text/x-rst
author = Walter A. Boring IV
author_email = waboring@hemna.com
license = GPL-3.0
license_file = LICENSE
classifiers =
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
classifier =
Topic :: Communications :: Ham Radio
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
description_file =
README.rst
summary = HAM Radio APRSD that reports weather from a weewx weather station.
[global]
setup-hooks =
pbr.hooks.setup_hook
[files]
packages =
aprsd_weewx_plugin
[build_sphinx]
source-dir = doc/source
build-dir = doc/build
all_files = 1
[upload_sphinx]
upload-dir = doc/build/html
[mypy]
ignore_missing_imports = True
strict = True

View File

@ -1,4 +1,3 @@
#
# 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
@ -15,13 +14,4 @@
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(setup_requires=["pbr"], pbr=True)
setuptools.setup()

47
tox.ini
View File

@ -4,27 +4,16 @@
envlist =
fmt
lint
py{37,38,39}
py311
skip_missing_interpreters = true
[flake8]
# Use the more relaxed max line length permitted in PEP8.
max-line-length = 99
# This ignore is required by black.
extend-ignore = E203
extend-exclude =
venv
requires = tox-uv>=0.4.0
# This is the configuration for the tox-gh-actions plugin for GitHub Actions
# https://github.com/ymyzk/tox-gh-actions
# This section is not needed if not using GitHub Actions for CI.
[gh-actions]
python =
3.7: py37
3.8: py38, fmt, lint
3.9: py39
3.11: py311, fmt, lint
# Activate isolated build environment. tox will use a virtual environment
# to build a source distribution from the source tree. For build tools and
@ -32,36 +21,35 @@ python =
isolated_build = true
[testenv]
package = uv-editable
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements-dev.txt
pytest
commands =
# Use -bb to enable BytesWarnings as error to catch str/bytes misuse.
# Use -Werror to treat warnings as errors.
{envpython} -bb -Werror -m pytest {posargs}
uv run pytest tests {posargs}
[testenv:type-check]
skip_install = true
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements-dev.txt
mypy
commands =
mypy src tests
[testenv:lint]
skip_install = true
deps =
-r{toxinidir}/requirements-dev.txt
ruff
commands =
flake8 aprsd_weewx_plugin tests
ruff check aprsd_weewx_plugin tests
[testenv:docs]
skip_install = true
package = uv-editable
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements-dev.txt
Sphinx
myst-parser
changedir = {toxinidir}/docs
commands =
python -c "import shutil; from pathlib import Path; Path('readme.md').write_text(Path('../README.md').read_text())"
python -c "import shutil; shutil.copy('../ChangeLog', 'changelog.rst')"
{envpython} clean_docs.py
sphinx-apidoc --force --output-dir apidoc {toxinidir}/aprsd_weewx_plugin
sphinx-build -a -W . _build
@ -69,15 +57,16 @@ commands =
[testenv:fmt]
skip_install = true
deps =
-r{toxinidir}/requirements-dev.txt
ruff
commands =
gray aprsd_weewx_plugin tests
ruff check --fix aprsd_weewx_plugin tests
ruff format aprsd_weewx_plugin tests
[testenv:licenses]
skip_install = true
recreate = true
deps =
-r{toxinidir}/requirements.txt
-e {toxinidir}
pip-licenses
commands =
pip-licenses {posargs}