mirror of
https://github.com/hemna/aprsd-weewx-plugin.git
synced 2026-03-16 12:49:49 -04:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a0cc61c41 | |||
| db9f7f982d | |||
| faa328396c | |||
| a3f5510bfb | |||
| fa012069c1 | |||
| 93095879b9 | |||
| c71a7d3598 | |||
| 6cbafdd1d2 | |||
| 153e2105b2 | |||
| 8e75f96c03 | |||
| 872ffd6ce4 | |||
| e59fa28cb1 | |||
| e4904f6d56 | |||
| 6c1ae8202d | |||
| 1da5037f49 | |||
| 39a290383a | |||
| 9bf8321d15 | |||
| 514612fa05 | |||
| bb7a5ed08e | |||
| a5ef1f10d4 | |||
| d7260fbd71 | |||
| 618fb85358 | |||
| eb0e5f55be | |||
| 87f6dfb654 | |||
| e72346c121 | |||
| 764535a016 |
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal 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']
|
||||
@ -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]
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@ -1 +1,2 @@
|
||||
Hemna <waboring@hemna.com>
|
||||
Walter A. Boring IV <waboring@hemna.com>
|
||||
|
||||
32
ChangeLog
32
ChangeLog
@ -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
|
||||
|
||||
8
Makefile
8
Makefile
@ -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
139
README.md
Normal file
@ -0,0 +1,139 @@
|
||||
# APRSD Weewx Plugin
|
||||
|
||||
[](https://pypi.org/project/aprsd-weewx-plugin/)
|
||||
[](https://pypi.org/project/aprsd-weewx-plugin/)
|
||||
[](https://pypi.org/project/aprsd-weewx-plugin)
|
||||
[](https://opensource.org/licenses/GNU%20GPL%20v3.0)
|
||||
|
||||
[](https://aprsd-weewx-plugin.readthedocs.io/)
|
||||
[](https://github.com/hemna/aprsd-weewx-plugin/actions?workflow=Tests)
|
||||
[](https://codecov.io/gh/hemna/aprsd-weewx-plugin)
|
||||
[](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.
|
||||
99
README.rst
99
README.rst
@ -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
|
||||
@ -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
|
||||
@ -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
50
aprsd_weewx_plugin/cli.py
Normal 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()
|
||||
6
aprsd_weewx_plugin/conf/__init__.py
Normal file
6
aprsd_weewx_plugin/conf/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd_weewx_plugin.conf import main
|
||||
|
||||
CONF = cfg.CONF
|
||||
main.register_opts(CONF)
|
||||
63
aprsd_weewx_plugin/conf/main.py
Normal file
63
aprsd_weewx_plugin/conf/main.py
Normal 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,
|
||||
}
|
||||
146
aprsd_weewx_plugin/conf/opts.py
Normal file
146
aprsd_weewx_plugin/conf/opts.py
Normal 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
|
||||
63
aprsd_weewx_plugin/conf/weewx.py
Normal file
63
aprsd_weewx_plugin/conf/weewx.py
Normal 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
371
aprsd_weewx_plugin/weewx.py
Normal 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
|
||||
37
docs/apidoc/aprsd_weewx_plugin.conf.rst
Normal file
37
docs/apidoc/aprsd_weewx_plugin.conf.rst
Normal 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:
|
||||
37
docs/apidoc/aprsd_weewx_plugin.rst
Normal file
37
docs/apidoc/aprsd_weewx_plugin.rst
Normal 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
7
docs/apidoc/modules.rst
Normal file
@ -0,0 +1,7 @@
|
||||
aprsd_weewx_plugin
|
||||
==================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
aprsd_weewx_plugin
|
||||
@ -1 +1,4 @@
|
||||
.. include:: ../AUTHORS.rst
|
||||
Authors
|
||||
=======
|
||||
|
||||
.. include:: ../AUTHORS
|
||||
|
||||
39
docs/changelog.rst
Normal file
39
docs/changelog.rst
Normal 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
|
||||
32
docs/conf.py
32
docs/conf.py
@ -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",
|
||||
|
||||
@ -1 +1,4 @@
|
||||
Contributing
|
||||
============
|
||||
|
||||
.. include:: ../CONTRIBUTING.rst
|
||||
|
||||
@ -1 +1,4 @@
|
||||
.. include:: ../HISTORY.rst
|
||||
History
|
||||
========
|
||||
|
||||
.. include:: ../ChangeLog
|
||||
|
||||
@ -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
|
||||
==================
|
||||
|
||||
@ -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
50
docs/readme.md
Normal file
@ -0,0 +1,50 @@
|
||||
# APRSD Weewx Plugin
|
||||
|
||||
[](https://pypi.org/project/aprsd-weewx-plugin/)
|
||||
[](https://pypi.org/project/aprsd-weewx-plugin/)
|
||||
[](https://pypi.org/project/aprsd-weewx-plugin)
|
||||
[](https://opensource.org/licenses/GNU%20GPL%20v3.0)
|
||||
|
||||
[](https://aprsd-weewx-plugin.readthedocs.io/)
|
||||
[](https://github.com/hemna/aprsd-weewx-plugin/actions?workflow=Tests)
|
||||
[](https://codecov.io/gh/hemna/aprsd-weewx-plugin)
|
||||
[](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
145
docs/usage.rst
Normal 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
154
pyproject.toml
Normal 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]
|
||||
@ -1,12 +0,0 @@
|
||||
pip
|
||||
pip-tools
|
||||
bump2version
|
||||
wheel
|
||||
watchdog
|
||||
flake8
|
||||
tox
|
||||
coverage
|
||||
Sphinx
|
||||
twine
|
||||
pytest==6.2.5
|
||||
gray
|
||||
@ -1,2 +0,0 @@
|
||||
pbr
|
||||
aprsd>=2.2.0
|
||||
40
setup.cfg
40
setup.cfg
@ -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
|
||||
12
setup.py
12
setup.py
@ -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
47
tox.ini
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user