Merge pull request #107 from craigerl/oslo-config

Convert config to oslo_config
This commit is contained in:
Walter A. Boring IV 2022-12-29 09:17:51 -05:00 committed by GitHub
commit 473f00973b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2872 additions and 2402 deletions

View File

@ -1,9 +1,43 @@
CHANGES
=======
* Decouple admin web interface from server command
* Dockerfile now produces aprsd.conf
* Fix some unit tests and loading of CONF w/o file
* Added missing conf
* Removed references to old custom config
* Convert config to oslo\_config
* Added rain formatting unit tests to WeatherPacket
* Fix Rain reporting in WeatherPacket send
* Removed Packet.send()
* Removed watchlist plugins
* Fix PluginManager.get\_plugins
* Cleaned up PluginManager
* Cleaned up PluginManager
* Update routing for weatherpacket
* Fix some WeatherPacket formatting
* Fix pep8 violation
* Add packet filtering for aprsd listen
* Added WeatherPacket encoding
* Updated webchat and listen for queue based RX
* reworked collecting and reporting stats
* Removed unused threading code
* Change RX packet processing to enqueu
* Make tracking objectstores work w/o initializing
* Cleaned up packet transmit class attributes
* Fix packets timestamp to int
* More messaging -> packets cleanup
* Cleaned out all references to messaging
* Added contructing a GPSPacket for sending
* cleanup webchat
* Reworked all packet processing
* Updated plugins and plugin interfaces for Packet
* Started using dataclasses to describe packets
v2.6.1
------
* v2.6.1
* Fixed position report for webchat beacon
* Try and fix broken 32bit qemu builds on 64bit system
* Add unit tests for webchat

View File

@ -22,7 +22,7 @@ dev: venv ## Create a python virtual environment for development of aprsd
run: venv ## Create a virtual environment for running aprsd commands
docs: build
docs: dev
cp README.rst docs/readme.rst
cp Changelog docs/changelog.rst
tox -edocs

View File

@ -52,39 +52,49 @@ Current list of built-in plugins
::
└─> aprsd list-plugins
🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimeOpenCageDataPlugin │ Current time of GPS beacon timezone. Uses OpenCage │ RegexCommand │ aprsd.plugins.time.TimeOpenCageDataPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└────────────────────────┴────────────────────────────────────────────────────────────┴──────────────┴───────────────────────────────────────────┘
🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
Pypi.org APRSD Installable Plugin Packages
Pypi.org APRSD Installable Plugin Packages
Install any of the following plugins with pip install <Plugin Package Name>
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.2 │ Nov 9, 2021 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.2 │ Nov 9, 2021 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.4 │ Jan 15, 2021 │ No │
└──────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
Install any of the following plugins with 'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
Installation
=============
@ -99,10 +109,10 @@ Example usage
``aprsd -h``
Help
----
====
::
└─[$] > aprsd -h
└─> aprsd -h
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
Options:
@ -116,18 +126,19 @@ Help
healthcheck Check the health of the running aprsd server.
list-plugins List the built in plugins available to APRSD.
listen Listen to packets on the APRS-IS Network based on FILTER.
sample-config This dumps the config to stdout.
sample-config Generate a sample Config file from aprsd and all...
send-message Send a message to a callsign via APRS_IS.
server Start the aprsd server gateway process.
version Show the APRSD version.
webchat Web based HAM Radio chat program!
Commands
--------
========
Configuration
^^^^^^^^^^^^^
=============
This command outputs a sample config yml formatted block that you can edit
and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.config/aprsd/aprsd.yml``
@ -136,108 +147,10 @@ and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.con
::
└─> aprsd sample-config
aprs:
# Set enabled to False if there is no internet connectivity.
# This is useful for a direwolf KISS aprs connection only.
# Get the passcode for your callsign here:
# https://apps.magicbug.co.uk/passcode
enabled: true
host: rotate.aprs2.net
login: CALLSIGN
password: '00000'
port: 14580
aprsd:
dateformat: '%m/%d/%Y %I:%M:%S %p'
email:
enabled: true
imap:
debug: false
host: imap.gmail.com
login: IMAP_USERNAME
password: IMAP_PASSWORD
port: 993
use_ssl: true
shortcuts:
aa: 5551239999@vtext.com
cl: craiglamparter@somedomain.org
wb: 555309@vtext.com
smtp:
debug: false
host: smtp.gmail.com
login: SMTP_USERNAME
password: SMTP_PASSWORD
port: 465
use_ssl: false
enabled_plugins:
- aprsd.plugins.email.EmailPlugin
- aprsd.plugins.fortune.FortunePlugin
- aprsd.plugins.location.LocationPlugin
- aprsd.plugins.ping.PingPlugin
- aprsd.plugins.query.QueryPlugin
- aprsd.plugins.stock.StockPlugin
- aprsd.plugins.time.TimePlugin
- aprsd.plugins.weather.USWeatherPlugin
- aprsd.plugins.version.VersionPlugin
logfile: /tmp/aprsd.log
logformat: '[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s] %(message)s
- [%(pathname)s:%(lineno)d]'
rich_logging: false
save_location: /Users/i530566/.config/aprsd/
trace: false
units: imperial
watch_list:
alert_callsign: NOCALL
alert_time_seconds: 43200
callsigns: []
enabled: false
enabled_plugins:
- aprsd.plugins.notify.NotifySeenPlugin
packet_keep_count: 10
web:
enabled: true
host: 0.0.0.0
logging_enabled: true
port: 8001
users:
admin: password-here
ham:
callsign: NOCALL
kiss:
serial:
baudrate: 9600
device: /dev/ttyS0
enabled: false
tcp:
enabled: false
host: direwolf.ip.address
port: '8001'
services:
aprs.fi:
# Get the apiKey from your aprs.fi account here:
# http://aprs.fi/account
apiKey: APIKEYVALUE
avwx:
# (Optional for AVWXWeatherPlugin)
# Use hosted avwx-api here: https://avwx.rest
# or deploy your own from here:
# https://github.com/avwx-rest/avwx-api
apiKey: APIKEYVALUE
base_url: http://host:port
opencagedata:
# (Optional for TimeOpenCageDataPlugin)
# Get the apiKey from your opencagedata account here:
# https://opencagedata.com/dashboard#api-keys
apiKey: APIKEYVALUE
openweathermap:
# (Optional for OWMWeatherPlugin)
# Get the apiKey from your
# openweathermap account here:
# https://home.openweathermap.org/api_keys
apiKey: APIKEYVALUE
...
server
^^^^^^
======
This is the main server command that will listen to APRS-IS servers and
look for incomming commands to the callsign configured in the config file
@ -277,7 +190,7 @@ look for incomming commands to the callsign configured in the config file
send-message
^^^^^^^^^^^^
============
This command is typically used for development to send another aprsd instance
test messages
@ -310,7 +223,7 @@ test messages
SEND EMAIL (radio to smtp server)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
=================================
::
@ -332,7 +245,7 @@ SEND EMAIL (radio to smtp server)
RECEIVE EMAIL (imap server to radio)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
====================================
::
@ -349,7 +262,7 @@ RECEIVE EMAIL (imap server to radio)
LOCATION
^^^^^^^^
========
::
@ -384,7 +297,7 @@ Development
* ``make``
Workflow
--------
========
While working aprsd, The workflow is as follows:
@ -413,7 +326,7 @@ While working aprsd, The workflow is as follows:
Release
-------
=======
To do release to pypi:
@ -435,10 +348,10 @@ To do release to pypi:
Docker Container
----------------
================
Building
^^^^^^^^
========
There are 2 versions of the container Dockerfile that can be used.
The main Dockerfile, which is for building the official release container
@ -447,18 +360,18 @@ which is used for building a container based off of a git branch of
the repo.
Official Build
^^^^^^^^^^^^^^
==============
``docker build -t hemna6969/aprsd:latest .``
Development Build
^^^^^^^^^^^^^^^^^
=================
``docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .``
Running the container
^^^^^^^^^^^^^^^^^^^^^
=====================
There is a ``docker-compose.yml`` file in the ``docker/`` directory
that can be used to run your container. To provide the container

View File

@ -21,6 +21,8 @@
# python included libs
import datetime
from importlib.metadata import entry_points
from importlib.metadata import version as metadata_version
import logging
import os
import signal
@ -29,19 +31,20 @@ import time
import click
import click_completion
from oslo_config import cfg, generator
# local imports here
import aprsd
from aprsd import cli_helper
from aprsd import config as aprsd_config
from aprsd import packets, stats, threads, utils
from aprsd import cli_helper, packets, stats, threads, utils
# setup the global logger
# logging.basicConfig(level=logging.DEBUG) # level=10
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
flask_enabled = False
rpc_serv = None
def custom_startswith(string, incomplete):
@ -91,6 +94,7 @@ def signal_handler(sig, frame):
LOG.info(stats.APRSDStats())
# signal.signal(signal.SIGTERM, sys.exit(0))
# sys.exit(0)
if flask_enabled:
signal.signal(signal.SIGTERM, sys.exit(0))
@ -111,8 +115,32 @@ def check_version(ctx):
@cli.command()
@click.pass_context
def sample_config(ctx):
"""This dumps the config to stdout."""
click.echo(aprsd_config.dump_default_cfg())
"""Generate a sample Config file from aprsd and all installed plugins."""
def get_namespaces():
args = []
selected = entry_points(group="oslo.config.opts")
for entry in selected:
if "aprsd" in entry.name:
args.append("--namespace")
args.append(entry.name)
return args
args = get_namespaces()
config_version = metadata_version("oslo.config")
logging.basicConfig(level=logging.WARN)
conf = cfg.ConfigOpts()
generator.register_cli_opts(conf)
try:
conf(args, version=config_version)
except cfg.RequiredOptError:
conf.print_help()
if not sys.argv[1:]:
raise SystemExit
raise
generator.generate(conf)
@cli.command()

View File

@ -1,13 +1,24 @@
from functools import update_wrapper
import logging
from pathlib import Path
import typing as t
import click
from oslo_config import cfg
from aprsd import config as aprsd_config
import aprsd
from aprsd import conf # noqa: F401
from aprsd.logging import log
from aprsd.utils import trace
CONF = cfg.CONF
home = str(Path.home())
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.conf"
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
common_options = [
@ -27,7 +38,7 @@ common_options = [
"--config",
"config_file",
show_default=True,
default=aprsd_config.DEFAULT_CONFIG_FILE,
default=DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.",
),
click.option(
@ -51,18 +62,32 @@ def process_standard_options(f: F) -> F:
def new_func(*args, **kwargs):
ctx = args[0]
ctx.ensure_object(dict)
config_file_found = True
if kwargs["config_file"]:
default_config_files = [kwargs["config_file"]]
else:
default_config_files = None
try:
CONF(
[], project="aprsd", version=aprsd.__version__,
default_config_files=default_config_files,
)
except cfg.ConfigFilesNotFoundError:
config_file_found = False
ctx.obj["loglevel"] = kwargs["loglevel"]
ctx.obj["config_file"] = kwargs["config_file"]
# ctx.obj["config_file"] = kwargs["config_file"]
ctx.obj["quiet"] = kwargs["quiet"]
ctx.obj["config"] = aprsd_config.parse_config(kwargs["config_file"])
log.setup_logging(
ctx.obj["config"],
ctx.obj["loglevel"],
ctx.obj["quiet"],
)
if ctx.obj["config"]["aprsd"].get("trace", False):
if CONF.trace_enabled:
trace.setup_tracing(["method", "api"])
if not config_file_found:
LOG = logging.getLogger("APRSD") # noqa: N806
LOG.error("No config file found!! run 'aprsd sample-config'")
del kwargs["loglevel"]
del kwargs["config_file"]
del kwargs["quiet"]

View File

@ -4,14 +4,15 @@ import time
import aprslib
from aprslib.exceptions import LoginError
from oslo_config import cfg
from aprsd import config as aprsd_config
from aprsd import exception
from aprsd.clients import aprsis, kiss
from aprsd.packets import core, packet_list
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
TRANSPORT_APRSIS = "aprsis"
TRANSPORT_TCPKISS = "tcpkiss"
@ -28,7 +29,6 @@ class Client:
_instance = None
_client = None
config = None
connected = False
server_string = None
@ -41,11 +41,6 @@ class Client:
# Put any initialization here.
return cls._instance
def __init__(self, config=None):
"""Initialize the object instance."""
if config:
self.config = config
def set_filter(self, filter):
self.filter = filter
if self._client:
@ -74,12 +69,12 @@ class Client:
@staticmethod
@abc.abstractmethod
def is_enabled(config):
def is_enabled():
pass
@staticmethod
@abc.abstractmethod
def transport(config):
def transport():
pass
@abc.abstractmethod
@ -90,26 +85,38 @@ class Client:
class APRSISClient(Client):
@staticmethod
def is_enabled(config):
def is_enabled():
# Defaults to True if the enabled flag is non existent
try:
return config["aprs"].get("enabled", True)
return CONF.aprs_network.enabled
except KeyError:
return False
@staticmethod
def is_configured(config):
if APRSISClient.is_enabled(config):
def is_configured():
if APRSISClient.is_enabled():
# Ensure that the config vars are correctly set
config.check_option("aprs.login")
config.check_option("aprs.password")
config.check_option("aprs.host")
return True
if not CONF.aprs_network.login:
LOG.error("Config aprs_network.login not set.")
raise exception.MissingConfigOptionException(
"aprs_network.login is not set.",
)
if not CONF.aprs_network.password:
LOG.error("Config aprs_network.password not set.")
raise exception.MissingConfigOptionException(
"aprs_network.password is not set.",
)
if not CONF.aprs_network.host:
LOG.error("Config aprs_network.host not set.")
raise exception.MissingConfigOptionException(
"aprs_network.host is not set.",
)
return True
return True
@staticmethod
def transport(config):
def transport():
return TRANSPORT_APRSIS
def decode_packet(self, *args, **kwargs):
@ -118,10 +125,10 @@ class APRSISClient(Client):
@trace.trace
def setup_connection(self):
user = self.config["aprs"]["login"]
password = self.config["aprs"]["password"]
host = self.config["aprs"].get("host", "rotate.aprs.net")
port = self.config["aprs"].get("port", 14580)
user = CONF.aprs_network.login
password = CONF.aprs_network.password
host = CONF.aprs_network.host
port = CONF.aprs_network.port
connected = False
backoff = 1
aprs_client = None
@ -151,45 +158,43 @@ class APRSISClient(Client):
class KISSClient(Client):
@staticmethod
def is_enabled(config):
def is_enabled():
"""Return if tcp or serial KISS is enabled."""
if "kiss" not in config:
return False
if config.get("kiss.serial.enabled", default=False):
if CONF.kiss_serial.enabled:
return True
if config.get("kiss.tcp.enabled", default=False):
if CONF.kiss_tcp.enabled:
return True
return False
@staticmethod
def is_configured(config):
def is_configured():
# Ensure that the config vars are correctly set
if KISSClient.is_enabled(config):
config.check_option(
"aprsd.callsign",
default_fail=aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
)
transport = KISSClient.transport(config)
if KISSClient.is_enabled():
transport = KISSClient.transport()
if transport == TRANSPORT_SERIALKISS:
config.check_option("kiss.serial")
config.check_option("kiss.serial.device")
if not CONF.kiss_serial.device:
LOG.error("KISS serial enabled, but no device is set.")
raise exception.MissingConfigOptionException(
"kiss_serial.device is not set.",
)
elif transport == TRANSPORT_TCPKISS:
config.check_option("kiss.tcp")
config.check_option("kiss.tcp.host")
config.check_option("kiss.tcp.port")
if not CONF.kiss_tcp.host:
LOG.error("KISS TCP enabled, but no host is set.")
raise exception.MissingConfigOptionException(
"kiss_tcp.host is not set.",
)
return True
return True
return False
@staticmethod
def transport(config):
if config.get("kiss.serial.enabled", default=False):
def transport():
if CONF.kiss_serial.enabled:
return TRANSPORT_SERIALKISS
if config.get("kiss.tcp.enabled", default=False):
if CONF.kiss_tcp.enabled:
return TRANSPORT_TCPKISS
def decode_packet(self, *args, **kwargs):
@ -208,7 +213,7 @@ class KISSClient(Client):
@trace.trace
def setup_connection(self):
client = kiss.KISS3Client(self.config)
client = kiss.KISS3Client()
return client
@ -222,8 +227,7 @@ class ClientFactory:
# Put any initialization here.
return cls._instance
def __init__(self, config):
self.config = config
def __init__(self):
self._builders = {}
def register(self, key, builder):
@ -231,23 +235,23 @@ class ClientFactory:
def create(self, key=None):
if not key:
if APRSISClient.is_enabled(self.config):
if APRSISClient.is_enabled():
key = TRANSPORT_APRSIS
elif KISSClient.is_enabled(self.config):
key = KISSClient.transport(self.config)
elif KISSClient.is_enabled():
key = KISSClient.transport()
LOG.debug(f"GET client '{key}'")
builder = self._builders.get(key)
if not builder:
raise ValueError(key)
return builder(self.config)
return builder()
def is_client_enabled(self):
"""Make sure at least one client is enabled."""
enabled = False
for key in self._builders.keys():
try:
enabled |= self._builders[key].is_enabled(self.config)
enabled |= self._builders[key].is_enabled()
except KeyError:
pass
@ -257,7 +261,7 @@ class ClientFactory:
enabled = False
for key in self._builders.keys():
try:
enabled |= self._builders[key].is_configured(self.config)
enabled |= self._builders[key].is_configured()
except KeyError:
pass
except exception.MissingConfigOptionException as ex:
@ -270,11 +274,11 @@ class ClientFactory:
return enabled
@staticmethod
def setup(config):
def setup():
"""Create and register all possible client objects."""
global factory
factory = ClientFactory(config)
factory = ClientFactory()
factory.register(TRANSPORT_APRSIS, APRSISClient)
factory.register(TRANSPORT_TCPKISS, KISSClient)
factory.register(TRANSPORT_SERIALKISS, KISSClient)

View File

@ -6,13 +6,15 @@
import logging
import click
from oslo_config import cfg
# local imports here
from aprsd import cli_helper, client, packets, plugin, stats, utils
from aprsd import cli_helper, client, conf, packets, plugin
from aprsd.aprsd import cli
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
@ -68,50 +70,40 @@ def test_plugin(
message,
):
"""Test an individual APRSD plugin given a python path."""
config = ctx.obj["config"]
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
CONF.log_opt_values(LOG, logging.DEBUG)
if not aprs_login:
if not config.exists("aprs.login"):
if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN:
click.echo("Must set --aprs_login or APRS_LOGIN")
ctx.exit(-1)
return
else:
fromcall = config.get("aprs.login")
fromcall = CONF.aprs_network.login
else:
fromcall = aprs_login
if not plugin_path:
click.echo(ctx.get_help())
click.echo("")
ctx.fail("Failed to provide -p option to test a plugin")
click.echo("Failed to provide -p option to test a plugin")
ctx.exit(-1)
return
if type(message) is tuple:
message = " ".join(message)
if config["aprsd"].get("trace", False):
if CONF.trace_enabled:
trace.setup_tracing(["method", "api"])
client.Client(config)
stats.APRSDStats(config)
packets.PacketTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
client.Client()
pm = plugin.PluginManager(config)
pm = plugin.PluginManager()
if load_all:
pm.setup_plugins()
else:
pm._init()
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase)
if not obj:
click.echo(ctx.get_help())
click.echo("")
@ -125,14 +117,13 @@ def test_plugin(
),
)
pm._pluggy_pm.register(obj)
login = config["aprs"]["login"]
packet = {
"from": fromcall, "addresse": login,
"message_text": message,
"format": "message",
"msgNo": 1,
}
packet = packets.MessagePacket(
from_call=fromcall,
to_call=CONF.callsign,
msgNo=1,
message_text=message,
)
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
for x in range(number):

View File

@ -10,11 +10,12 @@ import sys
import time
import click
from oslo_config import cfg
from rich.console import Console
# local imports here
import aprsd
from aprsd import cli_helper, client, packets, stats, threads, utils
from aprsd import cli_helper, client, packets, stats, threads
from aprsd.aprsd import cli
from aprsd.threads import rx
@ -22,6 +23,7 @@ from aprsd.threads import rx
# setup the global logger
# logging.basicConfig(level=logging.DEBUG) # level=10
LOG = logging.getLogger("APRSD")
CONF = cfg.CONF
console = Console()
@ -38,8 +40,8 @@ def signal_handler(sig, frame):
class APRSDListenThread(rx.APRSDRXThread):
def __init__(self, config, packet_queue, packet_filter=None):
super().__init__(config, packet_queue)
def __init__(self, packet_queue, packet_filter=None):
super().__init__(packet_queue)
self.packet_filter = packet_filter
def process_packet(self, *args, **kwargs):
@ -118,7 +120,6 @@ def listen(
"""
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
config = ctx.obj["config"]
if not aprs_login:
click.echo(ctx.get_help())
@ -132,27 +133,19 @@ def listen(
ctx.fail("Must set --aprs-password or APRS_PASSWORD")
ctx.exit()
config["aprs"]["login"] = aprs_login
config["aprs"]["password"] = aprs_password
# CONF.aprs_network.login = aprs_login
# config["aprs"]["password"] = aprs_password
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
stats.APRSDStats(config)
CONF.log_opt_values(LOG, logging.DEBUG)
# Try and load saved MsgTrack list
LOG.debug("Loading saved MsgTrack object.")
# Initialize the client factory and create
# The correct client object ready for use
client.ClientFactory.setup(config)
client.ClientFactory.setup()
# Make sure we have 1 client transport enabled
if not client.factory.is_client_enabled():
LOG.error("No Clients are enabled in config.")
@ -166,12 +159,11 @@ def listen(
LOG.debug(f"Filter by '{filter}'")
aprs_client.set_filter(filter)
keepalive = threads.KeepAliveThread(config=config)
keepalive = threads.KeepAliveThread()
keepalive.start()
LOG.debug("Create APRSDListenThread")
listen_thread = APRSDListenThread(
config=config,
packet_queue=threads.packet_queue,
packet_filter=packet_filter,
)

View File

@ -5,13 +5,16 @@ import time
import aprslib
from aprslib.exceptions import LoginError
import click
from oslo_config import cfg
import aprsd
from aprsd import cli_helper, client, packets
from aprsd import conf # noqa : F401
from aprsd.aprsd import cli
from aprsd.threads import tx
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -62,24 +65,24 @@ def send_message(
):
"""Send a message to a callsign via APRS_IS."""
global got_ack, got_response
config = ctx.obj["config"]
quiet = ctx.obj["quiet"]
if not aprs_login:
if not config.exists("aprs.login"):
if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN:
click.echo("Must set --aprs_login or APRS_LOGIN")
ctx.exit(-1)
return
else:
config["aprs"]["login"] = aprs_login
else:
aprs_login = CONF.aprs_network.login
if not aprs_password:
if not config.exists("aprs.password"):
LOG.warning(CONF.aprs_network.password)
if not CONF.aprs_network.password:
click.echo("Must set --aprs-password or APRS_PASSWORD")
ctx.exit(-1)
return
else:
config["aprs"]["password"] = aprs_password
else:
aprs_password = CONF.aprs_network.password
LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}")
if type(command) is tuple:
@ -90,9 +93,9 @@ def send_message(
else:
LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'")
packets.PacketList(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
packets.PacketList()
packets.WatchList()
packets.SeenList()
got_ack = False
got_response = False
@ -109,7 +112,7 @@ def send_message(
else:
got_response = True
from_call = packet.from_call
our_call = config["aprsd"]["callsign"].lower()
our_call = CONF.callsign.lower()
tx.send(
packets.AckPacket(
from_call=our_call,
@ -127,7 +130,7 @@ def send_message(
sys.exit(0)
try:
client.ClientFactory.setup(config)
client.ClientFactory.setup()
client.factory.create().client
except LoginError:
sys.exit(-1)

View File

@ -3,16 +3,18 @@ import signal
import sys
import click
from oslo_config import cfg
import aprsd
from aprsd import (
cli_helper, client, flask, packets, plugin, stats, threads, utils,
cli_helper, client, packets, plugin, rpc_server, threads, utils,
)
from aprsd import aprsd as aprsd_main
from aprsd.aprsd import cli
from aprsd.threads import rx
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -32,17 +34,9 @@ LOG = logging.getLogger("APRSD")
@cli_helper.process_standard_options
def server(ctx, flush):
"""Start the aprsd server gateway process."""
ctx.obj["config_file"]
loglevel = ctx.obj["loglevel"]
quiet = ctx.obj["quiet"]
config = ctx.obj["config"]
signal.signal(signal.SIGINT, aprsd_main.signal_handler)
signal.signal(signal.SIGTERM, aprsd_main.signal_handler)
if not quiet:
click.echo("Load config")
level, msg = utils._check_version()
if level:
LOG.warning(msg)
@ -50,19 +44,11 @@ def server(ctx, flush):
LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}")
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
stats.APRSDStats(config)
CONF.log_opt_values(LOG, logging.DEBUG)
# Initialize the client factory and create
# The correct client object ready for use
client.ClientFactory.setup(config)
client.ClientFactory.setup()
# Make sure we have 1 client transport enabled
if not client.factory.is_client_enabled():
LOG.error("No Clients are enabled in config.")
@ -77,30 +63,28 @@ def server(ctx, flush):
client.factory.create().client
# Now load the msgTrack from disk if any
packets.PacketList(config=config)
packets.PacketList()
if flush:
LOG.debug("Deleting saved MsgTrack.")
packets.PacketTrack(config=config).flush()
packets.WatchList(config=config)
packets.SeenList(config=config)
packets.PacketTrack().flush()
packets.WatchList()
packets.SeenList()
else:
# Try and load saved MsgTrack list
LOG.debug("Loading saved MsgTrack object.")
packets.PacketTrack(config=config).load()
packets.WatchList(config=config).load()
packets.SeenList(config=config).load()
packets.PacketTrack().load()
packets.WatchList().load()
packets.SeenList().load()
# Create the initial PM singleton and Register plugins
LOG.info("Loading Plugin Manager and registering plugins")
plugin_manager = plugin.PluginManager(config)
plugin_manager = plugin.PluginManager()
plugin_manager.setup_plugins()
rx_thread = rx.APRSDPluginRXThread(
packet_queue=threads.packet_queue,
config=config,
)
process_thread = rx.APRSDPluginProcessPacketThread(
config=config,
packet_queue=threads.packet_queue,
)
rx_thread.start()
@ -108,21 +92,13 @@ def server(ctx, flush):
packets.PacketTrack().restart()
keepalive = threads.KeepAliveThread(config=config)
keepalive = threads.KeepAliveThread()
keepalive.start()
web_enabled = config.get("aprsd.web.enabled", default=False)
if CONF.rpc_settings.enabled:
rpc = rpc_server.APRSDRPCThread()
rpc.start()
log_monitor = threads.log_monitor.LogMonitorThread()
log_monitor.start()
if web_enabled:
aprsd_main.flask_enabled = True
(socketio, app) = flask.init_flask(config, loglevel, quiet)
socketio.run(
app,
allow_unsafe_werkzeug=True,
host=config["aprsd"]["web"]["host"],
port=config["aprsd"]["web"]["port"],
)
# If there are items in the msgTracker, then save them
LOG.info("APRSD Exiting.")
return 0

View File

@ -15,23 +15,24 @@ from flask.logging import default_handler
import flask_classful
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from oslo_config import cfg
from user_agents import parse as ua_parse
from werkzeug.security import check_password_hash, generate_password_hash
import wrapt
import aprsd
from aprsd import cli_helper, client
from aprsd import config as aprsd_config
from aprsd import packets, stats, threads, utils
from aprsd import cli_helper, client, conf, packets, stats, threads, utils
from aprsd.aprsd import cli
from aprsd.logging import rich as aprsd_logging
from aprsd.threads import rx, tx
from aprsd.utils import objectstore, trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth()
users = None
socketio = None
def signal_handler(sig, frame):
@ -128,16 +129,16 @@ class SentMessages(objectstore.ObjectStoreMixin):
def verify_password(username, password):
global users
if username in users and check_password_hash(users.get(username), password):
if username in users and check_password_hash(users[username], password):
return username
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
"""Class that handles packets being sent to us."""
def __init__(self, config, packet_queue, socketio):
def __init__(self, packet_queue, socketio):
self.socketio = socketio
self.connected = False
super().__init__(config, packet_queue)
super().__init__(packet_queue)
def process_ack_packet(self, packet: packets.AckPacket):
super().process_ack_packet(packet)
@ -174,21 +175,16 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
class WebChatFlask(flask_classful.FlaskView):
config = None
def set_config(self, config):
def set_config(self):
global users
self.config = config
self.users = {}
for user in self.config["aprsd"]["web"]["users"]:
self.users[user] = generate_password_hash(
self.config["aprsd"]["web"]["users"][user],
)
user = CONF.admin.user
self.users[user] = generate_password_hash(CONF.admin.password)
users = self.users
def _get_transport(self, stats):
if self.config["aprs"].get("enabled", True):
if CONF.aprs_network.enabled:
transport = "aprs-is"
aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >"
@ -196,27 +192,22 @@ class WebChatFlask(flask_classful.FlaskView):
)
else:
# We might be connected to a KISS socket?
if client.KISSClient.is_enabled(self.config):
transport = client.KISSClient.transport(self.config)
if client.KISSClient.is_enabled():
transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
)
)
elif transport == client.TRANSPORT_SERIALKISS:
# for pep8 violation
kiss_default = aprsd_config.DEFAULT_DATE_FORMAT["kiss"]
default_baudrate = kiss_default["serial"]["baudrate"]
aprs_connection = (
"SerialKISS://{}@{} baud".format(
self.config["kiss"]["serial"]["device"],
self.config["kiss"]["serial"].get(
"baudrate",
default_baudrate,
),
)
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
),
)
return transport, aprs_connection
@ -250,7 +241,7 @@ class WebChatFlask(flask_classful.FlaskView):
html_template,
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=self.config["aprsd"]["callsign"],
callsign=CONF.callsign,
version=aprsd.__version__,
)
@ -287,14 +278,12 @@ class WebChatFlask(flask_classful.FlaskView):
class SendMessageNamespace(Namespace):
"""Class to handle the socketio interactions."""
_config = None
got_ack = False
reply_sent = False
msg = None
request = None
def __init__(self, namespace=None, config=None):
self._config = config
super().__init__(namespace)
def on_connect(self):
@ -312,7 +301,7 @@ class SendMessageNamespace(Namespace):
global socketio
LOG.debug(f"WS: on_send {data}")
self.request = data
data["from"] = self._config["aprs"]["login"]
data["from"] = CONF.callsign
pkt = packets.MessagePacket(
from_call=data["from"],
to_call=data["to"].upper(),
@ -338,7 +327,7 @@ class SendMessageNamespace(Namespace):
tx.send(
packets.GPSPacket(
from_call=self._config["aprs"]["login"],
from_call=CONF.callsign,
to_call="APDW16",
latitude=lat,
longitude=long,
@ -354,25 +343,16 @@ class SendMessageNamespace(Namespace):
LOG.debug(f"WS json {data}")
def setup_logging(config, flask_app, loglevel, quiet):
def setup_logging(flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler)
log_level = aprsd_config.LOG_LEVELS[loglevel]
log_level = conf.log.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level)
date_format = config["aprsd"].get(
"dateformat",
aprsd_config.DEFAULT_DATE_FORMAT,
)
date_format = CONF.logging.date_format
if not config["aprsd"]["web"].get("logging_enabled", False):
# disable web logging
flask_log.disabled = True
flask_app.logger.disabled = True
# return
if config["aprsd"].get("rich_logging", False) and not quiet:
if CONF.logging.rich_logging and not quiet:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
@ -382,13 +362,10 @@ def setup_logging(config, flask_app, loglevel, quiet):
rh.setFormatter(log_formatter)
flask_log.addHandler(rh)
log_file = config["aprsd"].get("logfile", None)
log_file = CONF.logging.logfile
if log_file:
log_format = config["aprsd"].get(
"logformat",
aprsd_config.DEFAULT_LOG_FORMAT,
)
log_format = CONF.loging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
@ -399,7 +376,7 @@ def setup_logging(config, flask_app, loglevel, quiet):
@trace.trace
def init_flask(config, loglevel, quiet):
def init_flask(loglevel, quiet):
global socketio
flask_app = flask.Flask(
@ -408,9 +385,9 @@ def init_flask(config, loglevel, quiet):
static_folder="web/chat/static",
template_folder="web/chat/templates",
)
setup_logging(config, flask_app, loglevel, quiet)
setup_logging(flask_app, loglevel, quiet)
server = WebChatFlask()
server.set_config(config)
server.set_config()
flask_app.route("/", methods=["GET"])(server.index)
flask_app.route("/stats", methods=["GET"])(server.stats)
# flask_app.route("/send-message", methods=["GET"])(server.send_message)
@ -427,7 +404,7 @@ def init_flask(config, loglevel, quiet):
socketio.on_namespace(
SendMessageNamespace(
"/sendmsg", config=config,
"/sendmsg",
),
)
return socketio, flask_app
@ -457,17 +434,12 @@ def init_flask(config, loglevel, quiet):
@cli_helper.process_standard_options
def webchat(ctx, flush, port):
"""Web based HAM Radio chat program!"""
ctx.obj["config_file"]
loglevel = ctx.obj["loglevel"]
quiet = ctx.obj["quiet"]
config = ctx.obj["config"]
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
if not quiet:
click.echo("Load config")
level, msg = utils._check_version()
if level:
LOG.warning(msg)
@ -475,19 +447,11 @@ def webchat(ctx, flush, port):
LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}")
flat_config = utils.flatten_dict(config)
LOG.info("Using CONFIG values:")
for x in flat_config:
if "password" in x or "aprsd.web.users.admin" in x:
LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX")
else:
LOG.info(f"{x} = {flat_config[x]}")
stats.APRSDStats(config)
CONF.log_opt_values(LOG, logging.DEBUG)
# Initialize the client factory and create
# The correct client object ready for use
client.ClientFactory.setup(config)
client.ClientFactory.setup()
# Make sure we have 1 client transport enabled
if not client.factory.is_client_enabled():
LOG.error("No Clients are enabled in config.")
@ -497,32 +461,30 @@ def webchat(ctx, flush, port):
LOG.error("APRS client is not properly configured in config file.")
sys.exit(-1)
packets.PacketList(config=config)
packets.PacketTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
packets.PacketList()
packets.PacketTrack()
packets.WatchList()
packets.SeenList()
(socketio, app) = init_flask(config, loglevel, quiet)
(socketio, app) = init_flask(loglevel, quiet)
rx_thread = rx.APRSDPluginRXThread(
config=config,
packet_queue=threads.packet_queue,
)
rx_thread.start()
process_thread = WebChatProcessPacketThread(
config=config,
packet_queue=threads.packet_queue,
socketio=socketio,
)
process_thread.start()
keepalive = threads.KeepAliveThread(config=config)
keepalive = threads.KeepAliveThread()
LOG.info("Start KeepAliveThread")
keepalive.start()
LOG.info("Start socketio.run()")
socketio.run(
app,
ssl_context="adhoc",
host=config["aprsd"]["web"]["host"],
host=CONF.admin.web_ip,
port=port,
)

56
aprsd/conf/__init__.py Normal file
View File

@ -0,0 +1,56 @@
from oslo_config import cfg
from aprsd.conf import client, common, log, plugin_common, plugin_email
CONF = cfg.CONF
log.register_opts(CONF)
common.register_opts(CONF)
client.register_opts(CONF)
# plugins
plugin_common.register_opts(CONF)
plugin_email.register_opts(CONF)
def set_lib_defaults():
"""Update default value for configuration options from other namespace.
Example, oslo lib config options. This is needed for
config generator tool to pick these default value changes.
https://docs.openstack.org/oslo.config/latest/cli/
generator.html#modifying-defaults-from-other-namespaces
"""
# Update default value of oslo_log default_log_levels and
# logging_context_format_string config option.
set_log_defaults()
def set_log_defaults():
# logging.set_defaults(default_log_levels=logging.get_default_log_levels())
pass
def conf_to_dict():
"""Convert the CONF options to a single level dictionary."""
entries = {}
def _sanitize(opt, value):
"""Obfuscate values of options declared secret."""
return value if not opt.secret else "*" * 4
for opt_name in sorted(CONF._opts):
opt = CONF._get_opt_info(opt_name)["opt"]
val = str(_sanitize(opt, getattr(CONF, opt_name)))
entries[str(opt)] = val
for group_name in list(CONF._groups):
group_attr = CONF.GroupAttr(CONF, CONF._get_group(group_name))
for opt_name in sorted(CONF._groups[group_name]._opts):
opt = CONF._get_opt_info(opt_name, group_name)["opt"]
val = str(_sanitize(opt, getattr(group_attr, opt_name)))
gname_opt_name = f"{group_name}.{opt_name}"
entries[gname_opt_name] = val
return entries

102
aprsd/conf/client.py Normal file
View File

@ -0,0 +1,102 @@
"""
The options for logging setup
"""
from oslo_config import cfg
DEFAULT_LOGIN = "NOCALL"
aprs_group = cfg.OptGroup(
name="aprs_network",
title="APRS-IS Network settings",
)
kiss_serial_group = cfg.OptGroup(
name="kiss_serial",
title="KISS Serial device connection",
)
kiss_tcp_group = cfg.OptGroup(
name="kiss_tcp",
title="KISS TCP/IP Device connection",
)
aprs_opts = [
cfg.BoolOpt(
"enabled",
default=True,
help="Set enabled to False if there is no internet connectivity."
"This is useful for a direwolf KISS aprs connection only.",
),
cfg.StrOpt(
"login",
default=DEFAULT_LOGIN,
help="APRS Username",
),
cfg.StrOpt(
"password",
secret=True,
help="APRS Password "
"Get the passcode for your callsign here: "
"https://apps.magicbug.co.uk/passcode",
),
cfg.HostnameOpt(
"host",
default="noam.aprs2.net",
help="The APRS-IS hostname",
),
cfg.PortOpt(
"port",
default=14580,
help="APRS-IS port",
),
]
kiss_serial_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable Serial KISS interface connection.",
),
cfg.StrOpt(
"device",
help="Serial Device file to use. /dev/ttyS0",
),
cfg.IntOpt(
"baudrate",
default=9600,
help="The Serial device baud rate for communication",
),
]
kiss_tcp_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable Serial KISS interface connection.",
),
cfg.HostnameOpt(
"host",
help="The KISS TCP Host to connect to.",
),
cfg.PortOpt(
"port",
default=8001,
help="The KISS TCP/IP network port",
),
]
def register_opts(config):
config.register_group(aprs_group)
config.register_opts(aprs_opts, group=aprs_group)
config.register_group(kiss_serial_group)
config.register_group(kiss_tcp_group)
config.register_opts(kiss_serial_opts, group=kiss_serial_group)
config.register_opts(kiss_tcp_opts, group=kiss_tcp_group)
def list_opts():
return {
aprs_group.name: aprs_opts,
kiss_serial_group.name: kiss_serial_opts,
kiss_tcp_group.name: kiss_tcp_opts,
}

165
aprsd/conf/common.py Normal file
View File

@ -0,0 +1,165 @@
from oslo_config import cfg
APRSD_DEFAULT_MAGIC_WORD = "CHANGEME!!!"
admin_group = cfg.OptGroup(
name="admin",
title="Admin web interface settings",
)
watch_list_group = cfg.OptGroup(
name="watch_list",
title="Watch List settings",
)
rpc_group = cfg.OptGroup(
name="rpc_settings",
title="RPC Settings for admin <--> web",
)
aprsd_opts = [
cfg.StrOpt(
"callsign",
required=True,
help="Callsign to use for messages sent by APRSD",
),
cfg.BoolOpt(
"enable_save",
default=True,
help="Enable saving of watch list, packet tracker between restarts.",
),
cfg.StrOpt(
"save_location",
default="~/.config/aprsd",
help="Save location for packet tracking files.",
),
cfg.BoolOpt(
"trace_enabled",
default=False,
help="Enable code tracing",
),
cfg.StrOpt(
"units",
default="imperial",
help="Units for display, imperial or metric",
),
]
watch_list_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable the watch list feature. Still have to enable "
"the correct plugin. Built-in plugin to use is "
"aprsd.plugins.notify.NotifyPlugin",
),
cfg.ListOpt(
"callsigns",
help="Callsigns to watch for messsages",
),
cfg.StrOpt(
"alert_callsign",
help="The Ham Callsign to send messages to for watch list alerts.",
),
cfg.IntOpt(
"packet_keep_count",
default=10,
help="The number of packets to store.",
),
cfg.IntOpt(
"alert_time_seconds",
default=3600,
help="Time to wait before alert is sent on new message for "
"users in callsigns.",
),
]
admin_opts = [
cfg.BoolOpt(
"web_enabled",
default=False,
help="Enable the Admin Web Interface",
),
cfg.IPOpt(
"web_ip",
default="0.0.0.0",
help="The ip address to listen on",
),
cfg.PortOpt(
"web_port",
default=8001,
help="The port to listen on",
),
cfg.StrOpt(
"user",
default="admin",
help="The admin user for the admin web interface",
),
cfg.StrOpt(
"password",
secret=True,
help="Admin interface password",
),
]
rpc_opts = [
cfg.BoolOpt(
"enabled",
default=True,
help="Enable RPC calls",
),
cfg.StrOpt(
"ip",
default="localhost",
help="The ip address to listen on",
),
cfg.PortOpt(
"port",
default=18861,
help="The port to listen on",
),
cfg.StrOpt(
"magic_word",
default=APRSD_DEFAULT_MAGIC_WORD,
help="Magic word to authenticate requests between client/server",
),
]
enabled_plugins_opts = [
cfg.ListOpt(
"enabled_plugins",
default=[
"aprsd.plugins.email.EmailPlugin",
"aprsd.plugins.fortune.FortunePlugin",
"aprsd.plugins.location.LocationPlugin",
"aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.query.QueryPlugin",
"aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.OWMWeatherPlugin",
"aprsd.plugins.version.VersionPlugin",
],
help="Comma separated list of enabled plugins for APRSD."
"To enable installed external plugins add them here."
"The full python path to the class name must be used",
),
]
def register_opts(config):
config.register_opts(aprsd_opts)
config.register_opts(enabled_plugins_opts)
config.register_group(admin_group)
config.register_opts(admin_opts, group=admin_group)
config.register_group(watch_list_group)
config.register_opts(watch_list_opts, group=watch_list_group)
config.register_group(rpc_group)
config.register_opts(rpc_opts, group=rpc_group)
def list_opts():
return {
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
admin_group.name: admin_opts,
watch_list_group.name: watch_list_opts,
rpc_group.name: rpc_opts,
}

61
aprsd/conf/log.py Normal file
View File

@ -0,0 +1,61 @@
"""
The options for logging setup
"""
import logging
from oslo_config import cfg
LOG_LEVELS = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
DEFAULT_LOG_FORMAT = (
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
)
logging_group = cfg.OptGroup(
name="logging",
title="Logging options",
)
logging_opts = [
cfg.StrOpt(
"date_format",
default=DEFAULT_DATE_FORMAT,
help="Date format for log entries",
),
cfg.BoolOpt(
"rich_logging",
default=True,
help="Enable Rich logging",
),
cfg.StrOpt(
"logfile",
default=None,
help="File to log to",
),
cfg.StrOpt(
"logformat",
default=DEFAULT_LOG_FORMAT,
help="Log file format, unless rich_logging enabled.",
),
]
def register_opts(config):
config.register_group(logging_group)
config.register_opts(logging_opts, group=logging_group)
def list_opts():
return {
logging_group.name: (
logging_opts
),
}

80
aprsd/conf/opts.py Normal file
View File

@ -0,0 +1,80 @@
# Copyright 2015 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
This is the single point of entry to generate the sample configuration
file for Nova. It collects all the necessary info from the other modules
in this package. It is assumed that:
* every other module in this package has a 'list_opts' function which
return a dict where
* the keys are strings which are the group names
* the value of each key is a list of config options for that group
* the nova.conf package doesn't have further packages with config options
* this module is only used in the context of sample file generation
"""
import collections
import importlib
import os
import pkgutil
LIST_OPTS_FUNC_NAME = "list_opts"
def _tupleize(dct):
"""Take the dict of options and convert to the 2-tuple format."""
return [(key, val) for key, val in dct.items()]
def list_opts():
opts = collections.defaultdict(list)
module_names = _list_module_names()
imported_modules = _import_modules(module_names)
_append_config_options(imported_modules, opts)
return _tupleize(opts)
def _list_module_names():
module_names = []
package_path = os.path.dirname(os.path.abspath(__file__))
for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]):
if modname == "opts" or ispkg:
continue
else:
module_names.append(modname)
return module_names
def _import_modules(module_names):
imported_modules = []
for modname in module_names:
mod = importlib.import_module("aprsd.conf." + modname)
if not hasattr(mod, LIST_OPTS_FUNC_NAME):
msg = "The module 'aprsd.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)

View File

@ -0,0 +1,83 @@
from oslo_config import cfg
aprsfi_group = cfg.OptGroup(
name="aprs_fi",
title="APRS.FI website settings",
)
query_group = cfg.OptGroup(
name="query_plugin",
title="Options for the Query Plugin",
)
avwx_group = cfg.OptGroup(
name="avwx_plugin",
title="Options for the AVWXWeatherPlugin",
)
owm_wx_group = cfg.OptGroup(
name="owm_weather_plugin",
title="Options for the OWMWeatherPlugin",
)
aprsfi_opts = [
cfg.StrOpt(
"apiKey",
help="Get the apiKey from your aprs.fi account here:"
"http://aprs.fi/account",
),
]
query_plugin_opts = [
cfg.StrOpt(
"callsign",
help="The Ham callsign to allow access to the query plugin from RF.",
),
]
owm_wx_opts = [
cfg.StrOpt(
"apiKey",
help="OWMWeatherPlugin api key to OpenWeatherMap's API."
"This plugin uses the openweathermap API to fetch"
"location and weather information."
"To use this plugin you need to get an openweathermap"
"account and apikey."
"https://home.openweathermap.org/api_keys",
),
]
avwx_opts = [
cfg.StrOpt(
"apiKey",
help="avwx-api is an opensource project that has"
"a hosted service here: https://avwx.rest/"
"You can launch your own avwx-api in a container"
"by cloning the githug repo here:"
"https://github.com/avwx-rest/AVWX-API",
),
cfg.StrOpt(
"base_url",
default="https://avwx.rest",
help="The base url for the avwx API. If you are hosting your own"
"Here is where you change the url to point to yours.",
),
]
def register_opts(config):
config.register_group(aprsfi_group)
config.register_opts(aprsfi_opts, group=aprsfi_group)
config.register_group(query_group)
config.register_opts(query_plugin_opts, group=query_group)
config.register_group(owm_wx_group)
config.register_opts(owm_wx_opts, group=owm_wx_group)
config.register_group(avwx_group)
config.register_opts(avwx_opts, group=avwx_group)
def list_opts():
return {
aprsfi_group.name: aprsfi_opts,
query_group.name: query_plugin_opts,
owm_wx_group.name: owm_wx_opts,
avwx_group.name: avwx_opts,
}

106
aprsd/conf/plugin_email.py Normal file
View File

@ -0,0 +1,106 @@
from oslo_config import cfg
email_group = cfg.OptGroup(
name="email_plugin",
title="Options for the APRSD Email plugin",
)
email_opts = [
cfg.StrOpt(
"callsign",
required=True,
help="(Required) Callsign to validate for doing email commands."
"Only this callsign can check email. This is also where the "
"email notifications for new emails will be sent.",
),
cfg.BoolOpt(
"enabled",
default=False,
help="Enable the Email plugin?",
),
cfg.BoolOpt(
"debug",
default=False,
help="Enable the Email plugin Debugging?",
),
]
email_imap_opts = [
cfg.StrOpt(
"imap_login",
help="Login username/email for IMAP server",
),
cfg.StrOpt(
"imap_password",
secret=True,
help="Login password for IMAP server",
),
cfg.HostnameOpt(
"imap_host",
help="Hostname/IP of the IMAP server",
),
cfg.PortOpt(
"imap_port",
default=993,
help="Port to use for IMAP server",
),
cfg.BoolOpt(
"imap_use_ssl",
default=True,
help="Use SSL for connection to IMAP Server",
),
]
email_smtp_opts = [
cfg.StrOpt(
"smtp_login",
help="Login username/email for SMTP server",
),
cfg.StrOpt(
"smtp_password",
secret=True,
help="Login password for SMTP server",
),
cfg.HostnameOpt(
"smtp_host",
help="Hostname/IP of the SMTP server",
),
cfg.PortOpt(
"smtp_port",
default=465,
help="Port to use for SMTP server",
),
cfg.BoolOpt(
"smtp_use_ssl",
default=True,
help="Use SSL for connection to SMTP Server",
),
]
email_shortcuts_opts = [
cfg.ListOpt(
"email_shortcuts",
help="List of email shortcuts for checking/sending email "
"For Exmaple: wb=walt@walt.com,cl=cl@cl.com\n"
"Means use 'wb' to send an email to walt@walt.com",
),
]
ALL_OPTS = (
email_opts
+ email_imap_opts
+ email_smtp_opts
+ email_shortcuts_opts
)
def register_opts(config):
config.register_group(email_group)
config.register_opts(ALL_OPTS, group=email_group)
def list_opts():
return {
email_group.name: ALL_OPTS,
}

View File

@ -1,404 +0,0 @@
import collections
import logging
import os
from pathlib import Path
import sys
import click
import yaml
from aprsd import exception, utils
home = str(Path.home())
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml"
LOG_LEVELS = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
DEFAULT_LOG_FORMAT = (
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
)
QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]"
QUEUE_LOG_FORMAT = (
"%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
)
CORE_MESSAGE_PLUGINS = [
"aprsd.plugins.email.EmailPlugin",
"aprsd.plugins.fortune.FortunePlugin",
"aprsd.plugins.location.LocationPlugin",
"aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.query.QueryPlugin",
"aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.USWeatherPlugin",
"aprsd.plugins.version.VersionPlugin",
]
CORE_NOTIFY_PLUGINS = [
"aprsd.plugins.notify.NotifySeenPlugin",
]
ALL_PLUGINS = []
for i in CORE_MESSAGE_PLUGINS:
ALL_PLUGINS.append(i)
for i in CORE_NOTIFY_PLUGINS:
ALL_PLUGINS.append(i)
# an example of what should be in the ~/.aprsd/config.yml
DEFAULT_CONFIG_DICT = {
"ham": {"callsign": "NOCALL"},
"aprs": {
"enabled": True,
# Only used as the login for aprsis.
"login": "CALLSIGN",
"password": "00000",
"host": "rotate.aprs2.net",
"port": 14580,
},
"kiss": {
"tcp": {
"enabled": False,
"host": "direwolf.ip.address",
"port": "8001",
},
"serial": {
"enabled": False,
"device": "/dev/ttyS0",
"baudrate": 9600,
},
},
"aprsd": {
# Callsign to use for all packets to/from aprsd instance
# regardless of the client (aprsis vs kiss)
"callsign": "NOCALL",
"logfile": "/tmp/aprsd.log",
"logformat": DEFAULT_LOG_FORMAT,
"dateformat": DEFAULT_DATE_FORMAT,
"save_location": DEFAULT_CONFIG_DIR,
"rich_logging": True,
"trace": False,
"enabled_plugins": ALL_PLUGINS,
"units": "imperial",
"watch_list": {
"enabled": False,
# Who gets the alert?
"alert_callsign": "NOCALL",
# 43200 is 12 hours
"alert_time_seconds": 43200,
# How many packets to save in a ring Buffer
# for a particular callsign
"packet_keep_count": 10,
"callsigns": [],
},
"web": {
"enabled": True,
"logging_enabled": True,
"host": "0.0.0.0",
"port": 8001,
"users": {
"admin": "password-here",
},
},
"email": {
"enabled": True,
"shortcuts": {
"aa": "5551239999@vtext.com",
"cl": "craiglamparter@somedomain.org",
"wb": "555309@vtext.com",
},
"smtp": {
"login": "SMTP_USERNAME",
"password": "SMTP_PASSWORD",
"host": "smtp.gmail.com",
"port": 465,
"use_ssl": False,
"debug": False,
},
"imap": {
"login": "IMAP_USERNAME",
"password": "IMAP_PASSWORD",
"host": "imap.gmail.com",
"port": 993,
"use_ssl": True,
"debug": False,
},
},
},
"services": {
"aprs.fi": {"apiKey": "APIKEYVALUE"},
"openweathermap": {"apiKey": "APIKEYVALUE"},
"opencagedata": {"apiKey": "APIKEYVALUE"},
"avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"},
},
}
class Config(collections.UserDict):
def _get(self, d, keys, default=None):
"""
Example:
d = {'meta': {'status': 'OK', 'status_code': 200}}
_get(d, ['meta', 'status_code']) # => 200
_get(d, ['garbage', 'status_code']) # => None
_get(d, ['meta', 'garbage'], default='-') # => '-'
"""
if type(keys) is str and "." in keys:
keys = keys.split(".")
assert type(keys) is list
if d is None:
return default
if not keys:
return d
if type(d) is str:
return default
return self._get(d.get(keys[0]), keys[1:], default)
def get(self, path, default=None):
return self._get(self.data, path, default=default)
def exists(self, path):
"""See if a conf value exists."""
test = "-3.14TEST41.3-"
return self.get(path, default=test) != test
def check_option(self, path, default_fail=None):
"""Make sure the config option doesn't have default value."""
if not self.exists(path):
if type(path) is list:
path = ".".join(path)
raise exception.MissingConfigOptionException(path)
val = self.get(path)
if val == default_fail:
# We have to fail and bail if the user hasn't edited
# this config option.
raise exception.ConfigOptionBogusDefaultException(
path, default_fail,
)
def add_config_comments(raw_yaml):
end_idx = utils.end_substr(raw_yaml, "ham:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign that owns this instance of APRSD.",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprsd:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Callsign to use for all APRSD Packets as the to/from."
"\n # regardless of client type (aprsis vs tcpkiss vs serial)",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprs:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Set enabled to False if there is no internet connectivity."
"\n # This is useful for a direwolf KISS aprs connection only. "
"\n"
"\n # Get the passcode for your callsign here: "
"\n # https://apps.magicbug.co.uk/passcode",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "aprs.fi:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # Get the apiKey from your aprs.fi account here: "
"\n # http://aprs.fi/account",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "opencagedata:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # (Optional for TimeOpenCageDataPlugin) "
"\n # Get the apiKey from your opencagedata account here: "
"\n # https://opencagedata.com/dashboard#api-keys",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "openweathermap:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # (Optional for OWMWeatherPlugin) "
"\n # Get the apiKey from your "
"\n # openweathermap account here: "
"\n # https://home.openweathermap.org/api_keys",
end_idx,
)
end_idx = utils.end_substr(raw_yaml, "avwx:")
if end_idx != -1:
# lets insert a comment
raw_yaml = utils.insert_str(
raw_yaml,
"\n # (Optional for AVWXWeatherPlugin) "
"\n # Use hosted avwx-api here: https://avwx.rest "
"\n # or deploy your own from here: "
"\n # https://github.com/avwx-rest/avwx-api",
end_idx,
)
return raw_yaml
def dump_default_cfg():
return add_config_comments(
yaml.dump(
DEFAULT_CONFIG_DICT,
indent=4,
),
)
def create_default_config():
"""Create a default config file."""
# make sure the directory location exists
config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE)
config_dir = os.path.dirname(config_file_expanded)
if not os.path.exists(config_dir):
click.echo(f"Config dir '{config_dir}' doesn't exist, creating.")
utils.mkdir_p(config_dir)
with open(config_file_expanded, "w+") as cf:
cf.write(dump_default_cfg())
def get_config(config_file):
"""This tries to read the yaml config from <config_file>."""
config_file_expanded = os.path.expanduser(config_file)
if os.path.exists(config_file_expanded):
with open(config_file_expanded) as stream:
config = yaml.load(stream, Loader=yaml.FullLoader)
return Config(config)
else:
if config_file == DEFAULT_CONFIG_FILE:
click.echo(
f"{config_file_expanded} is missing, creating config file",
)
create_default_config()
msg = (
"Default config file created at {}. Please edit with your "
"settings.".format(config_file)
)
click.echo(msg)
else:
# The user provided a config file path different from the
# Default, so we won't try and create it, just bitch and bail.
msg = f"Custom config file '{config_file}' is missing."
click.echo(msg)
sys.exit(-1)
# This method tries to parse the config yaml file
# and consume the settings.
# If the required params don't exist,
# it will look in the environment
def parse_config(config_file):
config = get_config(config_file)
def fail(msg):
click.echo(msg)
sys.exit(-1)
def check_option(config, path, default_fail=None):
try:
config.check_option(path, default_fail=default_fail)
except Exception as ex:
fail(repr(ex))
else:
return config
# special check here to make sure user has edited the config file
# and changed the ham callsign
check_option(
config,
"ham.callsign",
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
)
check_option(
config,
["aprsd"],
)
check_option(
config,
"aprsd.callsign",
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["callsign"],
)
# Ensure they change the admin password
if config.get("aprsd.web.enabled") is True:
check_option(
config,
["aprsd", "web", "users", "admin"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
)
if config.get("aprsd.watch_list.enabled") is True:
check_option(
config,
["aprsd", "watch_list", "alert_callsign"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
)
if config.get("aprsd.email.enabled") is True:
# Check IMAP server settings
check_option(config, ["aprsd", "email", "imap", "host"])
check_option(config, ["aprsd", "email", "imap", "port"])
check_option(
config,
["aprsd", "email", "imap", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
)
check_option(
config,
["aprsd", "email", "imap", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
)
# Check SMTP server settings
check_option(config, ["aprsd", "email", "smtp", "host"])
check_option(config, ["aprsd", "email", "smtp", "port"])
check_option(
config,
["aprsd", "email", "smtp", "login"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
)
check_option(
config,
["aprsd", "email", "smtp", "password"],
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
)
return config

View File

@ -2,100 +2,42 @@ import datetime
import json
import logging
from logging.handlers import RotatingFileHandler
import threading
import time
import aprslib
from aprslib.exceptions import LoginError
import flask
from flask import request
from flask.logging import default_handler
import flask_classful
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from oslo_config import cfg
import rpyc
from werkzeug.security import check_password_hash, generate_password_hash
import wrapt
import aprsd
from aprsd import client
from aprsd import config as aprsd_config
from aprsd import packets, plugin, stats, threads, utils
from aprsd.clients import aprsis
from aprsd.logging import log
from aprsd import cli_helper, client, conf, packets, plugin, threads
from aprsd.conf import common
from aprsd.logging import rich as aprsd_logging
from aprsd.threads import tx
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
auth = HTTPBasicAuth()
users = None
app = None
class SentMessages:
_instance = None
lock = threading.Lock()
class AuthSocketStream(rpyc.SocketStream):
"""Used to authenitcate the RPC stream to remote."""
msgs = {}
@classmethod
def connect(cls, *args, authorizer=None, **kwargs):
stream_obj = super().connect(*args, **kwargs)
def __new__(cls, *args, **kwargs):
"""This magic turns this into a singleton."""
if cls._instance is None:
cls._instance = super().__new__(cls)
# Put any initialization here.
return cls._instance
if callable(authorizer):
authorizer(stream_obj.sock)
@wrapt.synchronized(lock)
def add(self, packet):
self.msgs[packet.msgNo] = self._create(packet.msgNo)
self.msgs[packet.msgNo]["from"] = packet.from_call
self.msgs[packet.msgNo]["to"] = packet.to_call
self.msgs[packet.msgNo]["message"] = packet.message_text.rstrip("\n")
packet._build_raw()
self.msgs[packet.msgNo]["raw"] = packet.raw.rstrip("\n")
def _create(self, id):
return {
"id": id,
"ts": time.time(),
"ack": False,
"from": None,
"to": None,
"raw": None,
"message": None,
"status": None,
"last_update": None,
"reply": None,
}
@wrapt.synchronized(lock)
def __len__(self):
return len(self.msgs.keys())
@wrapt.synchronized(lock)
def get(self, id):
if id in self.msgs:
return self.msgs[id]
@wrapt.synchronized(lock)
def get_all(self):
return self.msgs
@wrapt.synchronized(lock)
def set_status(self, id, status):
self.msgs[id]["last_update"] = str(datetime.datetime.now())
self.msgs[id]["status"] = status
@wrapt.synchronized(lock)
def ack(self, id):
"""The message got an ack!"""
self.msgs[id]["last_update"] = str(datetime.datetime.now())
self.msgs[id]["ack"] = True
@wrapt.synchronized(lock)
def reply(self, id, packet):
"""We got a packet back from the sent message."""
self.msgs[id]["reply"] = packet
return stream_obj
# HTTPBasicAuth doesn't work on a class method.
@ -109,215 +51,168 @@ def verify_password(username, password):
return username
class SendMessageThread(threads.APRSDRXThread):
"""Thread for sending a message from web."""
class RPCClient:
_instance = None
_rpc_client = None
aprsis_client = None
request = None
got_ack = False
got_reply = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, config, info, packet, namespace):
self.config = config
self.request = info
self.packet = packet
self.namespace = namespace
self.start_time = datetime.datetime.now()
msg = "({} -> {}) : {}".format(
info["from"],
info["to"],
info["message"],
)
super().__init__(f"WEB_SEND_MSG-{msg}")
def __init__(self):
self._check_settings()
self.get_rpc_client()
def setup_connection(self):
user = self.request["from"]
password = self.request["password"]
host = self.config["aprs"].get("host", "rotate.aprs.net")
port = self.config["aprs"].get("port", 14580)
connected = False
backoff = 1
while not connected:
try:
LOG.info("Creating aprslib client")
def _check_settings(self):
if not CONF.rpc_settings.enabled:
LOG.error("RPC is not enabled, no way to get stats!!")
aprs_client = aprsis.Aprsdis(
user,
passwd=password,
host=host,
port=port,
)
# Force the logging to be the same
aprs_client.logger = LOG
aprs_client.connect()
connected = True
backoff = 1
except LoginError as e:
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
connected = False
raise e
except Exception as e:
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
time.sleep(backoff)
backoff = backoff * 2
continue
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
return aprs_client
if CONF.rpc_settings.magic_word == common.APRSD_DEFAULT_MAGIC_WORD:
LOG.warning("You are using the default RPC magic word!!!")
LOG.warning("edit aprsd.conf and change rpc_settings.magic_word")
def run(self):
LOG.debug("Starting")
from_call = self.request["from"]
to_call = self.request["to"]
message = self.request["message"]
LOG.info(
"From: '{}' To: '{}' Send '{}'".format(
from_call,
to_call,
message,
),
)
def _rpyc_connect(
self, host, port,
service=rpyc.VoidService,
config={}, ipv6=False,
keepalive=False, authorizer=None,
):
print(f"Connecting to RPC host {host}:{port}")
try:
self.aprs_client = self.setup_connection()
except LoginError as e:
f"Failed to setup Connection {e}"
tx.send(
self.packet,
direct=True,
aprs_client=self.aprs_client,
)
SentMessages().set_status(self.packet.msgNo, "Sent")
while not self.thread_stop:
can_loop = self.loop()
if not can_loop:
self.stop()
threads.APRSDThreadList().remove(self)
LOG.debug("Exiting")
def process_ack_packet(self, packet):
global socketio
ack_num = packet.msgNo
LOG.info(f"We got ack for our sent message {ack_num}")
packet.log("RXACK")
SentMessages().ack(self.packet.msgNo)
stats.APRSDStats().ack_rx_inc()
socketio.emit(
"ack", SentMessages().get(self.packet.msgNo),
namespace="/sendmsg",
)
if self.request["wait_reply"] == "0" or self.got_reply:
# We aren't waiting for a reply, so we can bail
self.stop()
self.thread_stop = self.aprs_client.thread_stop = True
def process_our_message_packet(self, packet):
global socketio
packets.PacketList().rx(packet)
stats.APRSDStats().msgs_rx_inc()
msg_number = packet.msgNo
SentMessages().reply(self.packet.msgNo, packet)
SentMessages().set_status(self.packet.msgNo, "Got Reply")
socketio.emit(
"reply", SentMessages().get(self.packet.msgNo),
namespace="/sendmsg",
)
tx.send(
packets.AckPacket(
from_call=self.request["from"],
to_call=packet.from_call,
msgNo=msg_number,
),
direct=True,
aprs_client=self.aprsis_client,
)
SentMessages().set_status(self.packet.msgNo, "Ack Sent")
# Now we can exit, since we are done.
self.got_reply = True
if self.got_ack:
self.stop()
self.thread_stop = self.aprs_client.thread_stop = True
def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs)
packet.log(header="RX Packet")
if isinstance(packet, packets.AckPacket):
self.process_ack_packet(packet)
else:
self.process_our_message_packet(packet)
def loop(self):
# we have a general time limit expecting results of
# around 120 seconds before we exit
now = datetime.datetime.now()
start_delta = str(now - self.start_time)
delta = utils.parse_delta_str(start_delta)
d = datetime.timedelta(**delta)
max_timeout = {"hours": 0.0, "minutes": 1, "seconds": 0}
max_delta = datetime.timedelta(**max_timeout)
if d > max_delta:
LOG.error("XXXXXX Haven't completed everything in 60 seconds. BAIL!")
return False
if self.got_ack and self.got_reply:
LOG.warning("We got everything already. BAIL")
return False
try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
self.aprs_client.consumer(
self.process_packet, raw=False, blocking=False,
s = AuthSocketStream.connect(
host, port, ipv6=ipv6, keepalive=keepalive,
authorizer=authorizer,
)
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped.")
return False
return rpyc.utils.factory.connect_stream(s, service, config=config)
except ConnectionRefusedError:
LOG.error(f"Failed to connect to RPC host {host}")
return None
return True
def get_rpc_client(self):
if not self._rpc_client:
magic = CONF.rpc_settings.magic_word
self._rpc_client = self._rpyc_connect(
CONF.rpc_settings.ip,
CONF.rpc_settings.port,
authorizer=lambda sock: sock.send(magic.encode()),
)
return self._rpc_client
def get_stats_dict(self):
cl = self.get_rpc_client()
result = {}
if not cl:
return result
try:
rpc_stats_dict = cl.root.get_stats()
result = json.loads(rpc_stats_dict)
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_packet_track(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result = cl.root.get_packet_track()
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_packet_list(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result = cl.root.get_packet_list()
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_watch_list(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result = cl.root.get_watch_list()
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_seen_list(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result = cl.root.get_seen_list()
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
def get_log_entries(self):
cl = self.get_rpc_client()
result = None
if not cl:
return result
try:
result_str = cl.root.get_log_entries()
result = json.loads(result_str)
except EOFError:
LOG.error("Lost connection to RPC Host")
self._rpc_client = None
return result
class APRSDFlask(flask_classful.FlaskView):
config = None
def set_config(self, config):
def set_config(self):
global users
self.config = config
self.users = {}
for user in self.config["aprsd"]["web"]["users"]:
self.users[user] = generate_password_hash(
self.config["aprsd"]["web"]["users"][user],
)
user = CONF.admin.user
self.users[user] = generate_password_hash(CONF.admin.password)
users = self.users
@auth.login_required
def index(self):
stats = self._stats()
print(stats)
LOG.debug(
"watch list? {}".format(
self.config["aprsd"]["watch_list"],
CONF.watch_list.callsigns,
),
)
wl = packets.WatchList()
if wl.is_enabled():
wl = RPCClient().get_watch_list()
if wl and wl.is_enabled():
watch_count = len(wl)
watch_age = wl.max_delta()
else:
watch_count = 0
watch_age = 0
sl = packets.SeenList()
seen_count = len(sl)
sl = RPCClient().get_seen_list()
if sl:
seen_count = len(sl)
else:
seen_count = 0
pm = plugin.PluginManager()
plugins = pm.get_plugins()
plugin_count = len(plugins)
if self.config["aprs"].get("enabled", True):
if CONF.aprs_network.enabled:
transport = "aprs-is"
aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >"
@ -325,33 +220,37 @@ class APRSDFlask(flask_classful.FlaskView):
)
else:
# We might be connected to a KISS socket?
if client.KISSClient.kiss_enabled(self.config):
transport = client.KISSClient.transport(self.config)
if client.KISSClient.kiss_enabled():
transport = client.KISSClient.transport()
if transport == client.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
)
)
elif transport == client.TRANSPORT_SERIALKISS:
aprs_connection = (
"SerialKISS://{}@{} baud".format(
self.config["kiss"]["serial"]["device"],
self.config["kiss"]["serial"]["baudrate"],
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
)
)
stats["transport"] = transport
stats["aprs_connection"] = aprs_connection
entries = conf.conf_to_dict()
return flask.render_template(
"index.html",
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=self.config["aprs"]["login"],
callsign=CONF.callsign,
version=aprsd.__version__,
config_json=json.dumps(self.config.data),
config_json=json.dumps(
entries, indent=4,
sort_keys=True, default=str,
),
watch_count=watch_count,
watch_age=watch_age,
seen_count=seen_count,
@ -368,32 +267,18 @@ class APRSDFlask(flask_classful.FlaskView):
return flask.render_template("messages.html", messages=json.dumps(msgs))
@auth.login_required
def send_message_status(self):
LOG.debug(request)
msgs = SentMessages()
info = msgs.get_all()
return json.dumps(info)
@auth.login_required
def send_message(self):
LOG.debug(request)
if request.method == "GET":
return flask.render_template(
"send-message.html",
callsign=self.config["aprs"]["login"],
version=aprsd.__version__,
)
@auth.login_required
def packets(self):
packet_list = packets.PacketList().get()
tmp_list = []
for pkt in packet_list:
tmp_list.append(pkt.json)
packet_list = RPCClient().get_packet_list()
if packet_list:
packets = packet_list.get()
tmp_list = []
for pkt in packets:
tmp_list.append(pkt.json)
LOG.info(f"PACKETS {tmp_list}")
return json.dumps(tmp_list)
return json.dumps(tmp_list)
else:
return json.dumps([])
@auth.login_required
def plugins(self):
@ -410,39 +295,69 @@ class APRSDFlask(flask_classful.FlaskView):
return json.dumps({"messages": "saved"})
def _stats(self):
stats_obj = stats.APRSDStats()
track = packets.PacketTrack()
track = RPCClient().get_packet_track()
now = datetime.datetime.now()
time_format = "%m-%d-%Y %H:%M:%S"
stats_dict = stats_obj.stats()
# Convert the watch_list entries to age
wl = packets.WatchList()
new_list = {}
for call in wl.get_all():
# call_date = datetime.datetime.strptime(
# str(wl.last_seen(call)),
# "%Y-%m-%d %H:%M:%S.%f",
# )
new_list[call] = {
"last": wl.age(call),
"packets": wl.get(call)["packets"].get(),
stats_dict = RPCClient().get_stats_dict()
if not stats_dict:
stats_dict = {
"aprsd": {},
"aprs-is": {"server": ""},
"messages": {
"sent": 0,
"received": 0,
},
"email": {
"sent": 0,
"received": 0,
},
"seen_list": {
"sent": 0,
"received": 0,
},
}
# Convert the watch_list entries to age
wl = RPCClient().get_watch_list()
new_list = {}
if wl:
for call in wl.get_all():
# call_date = datetime.datetime.strptime(
# str(wl.last_seen(call)),
# "%Y-%m-%d %H:%M:%S.%f",
# )
# We have to convert the RingBuffer to a real list
# so that json.dumps works.
# pkts = []
# for pkt in wl.get(call)["packets"].get():
# pkts.append(pkt)
new_list[call] = {
"last": wl.age(call),
# "packets": pkts
}
stats_dict["aprsd"]["watch_list"] = new_list
packet_list = packets.PacketList()
rx = packet_list.total_rx()
tx = packet_list.total_tx()
packet_list = RPCClient().get_packet_list()
rx = tx = 0
if packet_list:
rx = packet_list.total_rx()
tx = packet_list.total_tx()
stats_dict["packets"] = {
"sent": tx,
"received": rx,
}
if track:
size_tracker = len(track)
else:
size_tracker = 0
result = {
"time": now.strftime(time_format),
"size_tracker": len(track),
"size_tracker": size_tracker,
"stats": stats_dict,
}
@ -452,139 +367,58 @@ class APRSDFlask(flask_classful.FlaskView):
return json.dumps(self._stats())
class SendMessageNamespace(Namespace):
_config = None
got_ack = False
reply_sent = False
packet = None
request = None
def __init__(self, namespace=None, config=None):
self._config = config
super().__init__(namespace)
def on_connect(self):
global socketio
LOG.debug("Web socket connected")
socketio.emit(
"connected", {"data": "/sendmsg Connected"},
namespace="/sendmsg",
)
def on_disconnect(self):
LOG.debug("WS Disconnected")
def on_send(self, data):
global socketio
LOG.debug(f"WS: on_send {data}")
self.request = data
self.packet = packets.MessagePacket(
from_call=data["from"],
to_call=data["to"],
message_text=data["message"],
)
msgs = SentMessages()
msgs.add(self.packet)
msgs.set_status(self.packet.msgNo, "Sending")
socketio.emit(
"sent", SentMessages().get(self.packet.msgNo),
namespace="/sendmsg",
)
socketio.start_background_task(
self._start, self._config, data,
self.packet, self,
)
LOG.warning("WS: on_send: exit")
def _start(self, config, data, packet, namespace):
msg_thread = SendMessageThread(self._config, data, packet, self)
msg_thread.start()
def handle_message(self, data):
LOG.debug(f"WS Data {data}")
def handle_json(self, data):
LOG.debug(f"WS json {data}")
class LogMonitorThread(threads.APRSDThread):
class LogUpdateThread(threads.APRSDThread):
def __init__(self):
super().__init__("LogMonitorThread")
super().__init__("LogUpdate")
def loop(self):
global socketio
try:
record = log.logging_queue.get(block=True, timeout=5)
json_record = self.json_record(record)
socketio.emit(
"log_entry", json_record,
namespace="/logs",
)
except Exception:
# Just ignore thi
pass
if socketio:
log_entries = RPCClient().get_log_entries()
if log_entries:
for entry in log_entries:
socketio.emit(
"log_entry", entry,
namespace="/logs",
)
time.sleep(5)
return True
def json_record(self, record):
entry = {}
entry["filename"] = record.filename
entry["funcName"] = record.funcName
entry["levelname"] = record.levelname
entry["lineno"] = record.lineno
entry["module"] = record.module
entry["name"] = record.name
entry["pathname"] = record.pathname
entry["process"] = record.process
entry["processName"] = record.processName
if hasattr(record, "stack_info"):
entry["stack_info"] = record.stack_info
else:
entry["stack_info"] = None
entry["thread"] = record.thread
entry["threadName"] = record.threadName
entry["message"] = record.getMessage()
return entry
class LoggingNamespace(Namespace):
log_thread = None
def on_connect(self):
global socketio
LOG.debug("Web socket connected")
socketio.emit(
"connected", {"data": "/logs Connected"},
namespace="/logs",
)
self.log_thread = LogMonitorThread()
self.log_thread = LogUpdateThread()
self.log_thread.start()
def on_disconnect(self):
LOG.debug("WS Disconnected")
self.log_thread.stop()
LOG.debug("LOG Disconnected")
if self.log_thread:
self.log_thread.stop()
def setup_logging(config, flask_app, loglevel, quiet):
def setup_logging(flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler)
log_level = aprsd_config.LOG_LEVELS[loglevel]
log_level = conf.log.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level)
date_format = config["aprsd"].get(
"dateformat",
aprsd_config.DEFAULT_DATE_FORMAT,
)
date_format = CONF.logging.date_format
flask_log.disabled = True
flask_app.logger.disabled = True
if not config["aprsd"]["web"].get("logging_enabled", False):
# disable web logging
flask_log.disabled = True
flask_app.logger.disabled = True
return
if config["aprsd"].get("rich_logging", False) and not quiet:
if CONF.logging.rich_logging:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
@ -594,13 +428,10 @@ def setup_logging(config, flask_app, loglevel, quiet):
rh.setFormatter(log_formatter)
flask_log.addHandler(rh)
log_file = config["aprsd"].get("logfile", None)
log_file = CONF.logging.logfile
if log_file:
log_format = config["aprsd"].get(
"logformat",
aprsd_config.DEFAULT_LOG_FORMAT,
)
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
@ -610,7 +441,7 @@ def setup_logging(config, flask_app, loglevel, quiet):
flask_log.addHandler(fh)
def init_flask(config, loglevel, quiet):
def init_flask(loglevel, quiet):
global socketio
flask_app = flask.Flask(
@ -619,15 +450,13 @@ def init_flask(config, loglevel, quiet):
static_folder="web/admin/static",
template_folder="web/admin/templates",
)
setup_logging(config, flask_app, loglevel, quiet)
setup_logging(flask_app, loglevel, quiet)
server = APRSDFlask()
server.set_config(config)
server.set_config()
flask_app.route("/", methods=["GET"])(server.index)
flask_app.route("/stats", methods=["GET"])(server.stats)
flask_app.route("/messages", methods=["GET"])(server.messages)
flask_app.route("/packets", methods=["GET"])(server.packets)
flask_app.route("/send-message", methods=["GET"])(server.send_message)
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
flask_app.route("/save", methods=["GET"])(server.save)
flask_app.route("/plugins", methods=["GET"])(server.plugins)
@ -637,7 +466,21 @@ def init_flask(config, loglevel, quiet):
)
# import eventlet
# eventlet.monkey_patch()
gunicorn_logger = logging.getLogger("gunicorn.error")
flask_app.logger.handlers = gunicorn_logger.handlers
flask_app.logger.setLevel(gunicorn_logger.level)
socketio.on_namespace(SendMessageNamespace("/sendmsg", config=config))
socketio.on_namespace(LoggingNamespace("/logs"))
return socketio, flask_app
if __name__ == "aprsd.flask":
try:
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
CONF(
[], project="aprsd", version=aprsd.__version__,
default_config_files=[default_config_file],
)
except cfg.ConfigFilesNotFoundError:
pass
sio, app = init_flask("DEBUG", False)

View File

@ -4,10 +4,13 @@ from logging.handlers import RotatingFileHandler
import queue
import sys
from aprsd import config as aprsd_config
from oslo_config import cfg
from aprsd import conf
from aprsd.logging import rich as aprsd_logging
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
logging_queue = queue.Queue()
@ -15,13 +18,15 @@ logging_queue = queue.Queue()
# Setup the logging faciility
# to disable logging to stdout, but still log to file
# use the --quiet option on the cmdln
def setup_logging(config, loglevel, quiet):
log_level = aprsd_config.LOG_LEVELS[loglevel]
def setup_logging(loglevel, quiet):
log_level = conf.log.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
date_format = CONF.logging.date_format
rh = None
fh = None
rich_logging = False
if config["aprsd"].get("rich_logging", False) and not quiet:
if CONF.logging.get("rich_logging", False) and not quiet:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
@ -32,8 +37,8 @@ def setup_logging(config, loglevel, quiet):
LOG.addHandler(rh)
rich_logging = True
log_file = config["aprsd"].get("logfile", None)
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
log_file = CONF.logging.logfile
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
if log_file:
@ -42,16 +47,19 @@ def setup_logging(config, loglevel, quiet):
LOG.addHandler(fh)
imap_logger = None
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
if CONF.email_plugin.enabled and CONF.email_plugin.debug:
imap_logger = logging.getLogger("imapclient.imaplib")
imap_logger.setLevel(log_level)
imap_logger.addHandler(fh)
if rh:
imap_logger.addHandler(rh)
if fh:
imap_logger.addHandler(fh)
if config.get("aprsd.web.enabled", default=False):
if CONF.admin.web_enabled:
qh = logging.handlers.QueueHandler(logging_queue)
q_log_formatter = logging.Formatter(
fmt=aprsd_config.QUEUE_LOG_FORMAT,
datefmt=aprsd_config.QUEUE_DATE_FORMAT,
fmt=CONF.logging.logformat,
datefmt=CONF.logging.date_format,
)
qh.setFormatter(q_log_formatter)
LOG.addHandler(qh)
@ -65,10 +73,10 @@ def setup_logging(config, loglevel, quiet):
def setup_logging_no_config(loglevel, quiet):
log_level = aprsd_config.LOG_LEVELS[loglevel]
log_level = conf.log.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
log_format = aprsd_config.DEFAULT_LOG_FORMAT
date_format = aprsd_config.DEFAULT_DATE_FORMAT
log_format = CONF.logging.logformat
date_format = CONF.logging.date_format
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = NullHandler()

View File

@ -84,6 +84,9 @@ class Packet(metaclass=abc.ABCMeta):
else:
return default
def update_timestamp(self):
self.timestamp = _int_timestamp()
def prepare(self):
"""Do stuff here that is needed prior to sending over the air."""
# now build the raw message for sending

View File

@ -1,12 +1,14 @@
import logging
import threading
from oslo_config import cfg
import wrapt
from aprsd import stats, utils
from aprsd.packets import seen_list
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -15,7 +17,6 @@ class PacketList:
_instance = None
lock = threading.Lock()
config = None
packet_list: utils.RingBuffer = utils.RingBuffer(1000)
@ -25,17 +26,8 @@ class PacketList:
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
if "config" in kwargs:
cls._instance.config = kwargs["config"]
return cls._instance
def __init__(self, config=None):
if config:
self.config = config
def _is_initialized(self):
return self.config is not None
@wrapt.synchronized(lock)
def __iter__(self):
return iter(self.packet_list)

View File

@ -2,11 +2,13 @@ import datetime
import logging
import threading
from oslo_config import cfg
import wrapt
from aprsd.utils import objectstore
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -16,21 +18,14 @@ class SeenList(objectstore.ObjectStoreMixin):
_instance = None
lock = threading.Lock()
data: dict = {}
config = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
if "config" in kwargs:
if "config" in kwargs:
cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance._init_store()
cls._instance.data = {}
return cls._instance
def is_initialized(self):
return self.config is not None
@wrapt.synchronized(lock)
def update_seen(self, packet):
callsign = None

View File

@ -1,12 +1,16 @@
import datetime
import threading
from oslo_config import cfg
import wrapt
from aprsd.threads import tx
from aprsd.utils import objectstore
CONF = cfg.CONF
class PacketTrack(objectstore.ObjectStoreMixin):
"""Class to keep track of outstanding text messages.
@ -23,7 +27,6 @@ class PacketTrack(objectstore.ObjectStoreMixin):
_instance = None
_start_time = None
lock = threading.Lock()
config = None
data: dict = {}
total_tracked: int = 0
@ -32,14 +35,9 @@ class PacketTrack(objectstore.ObjectStoreMixin):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._start_time = datetime.datetime.now()
if "config" in kwargs:
cls._instance.config = kwargs["config"]
cls._instance._init_store()
return cls._instance
def is_initialized(self):
return self.config is not None
@wrapt.synchronized(lock)
def __getitem__(self, name):
return self.data[name]

View File

@ -2,12 +2,14 @@ import datetime
import logging
import threading
from oslo_config import cfg
import wrapt
from aprsd import utils
from aprsd.utils import objectstore
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -17,24 +19,19 @@ class WatchList(objectstore.ObjectStoreMixin):
_instance = None
lock = threading.Lock()
data = {}
config = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
if "config" in kwargs:
cls._instance.config = kwargs["config"]
cls._instance._init_store()
cls._instance._init_store()
cls._instance.data = {}
return cls._instance
def __init__(self, config=None):
if config:
self.config = config
ring_size = CONF.watch_list.packet_keep_count
ring_size = config["aprsd"]["watch_list"].get("packet_keep_count", 10)
for callsign in config["aprsd"]["watch_list"].get("callsigns", []):
if CONF.watch_list.callsigns:
for callsign in CONF.watch_list.callsigns:
call = callsign.replace("*", "")
# FIXME(waboring) - we should fetch the last time we saw
# a beacon from a callsign or some other mechanism to find
@ -47,14 +44,8 @@ class WatchList(objectstore.ObjectStoreMixin):
),
}
def is_initialized(self):
return self.config is not None
def is_enabled(self):
if self.config and "watch_list" in self.config["aprsd"]:
return self.config["aprsd"]["watch_list"].get("enabled", False)
else:
return False
return CONF.watch_list.enabled
def callsign_in_watchlist(self, callsign):
return callsign in self.data
@ -78,9 +69,8 @@ class WatchList(objectstore.ObjectStoreMixin):
return str(now - self.last_seen(callsign))
def max_delta(self, seconds=None):
watch_list_conf = self.config["aprsd"]["watch_list"]
if not seconds:
seconds = watch_list_conf["alert_time_seconds"]
seconds = CONF.watch_list.alert_time_seconds
max_timeout = {"seconds": seconds}
return datetime.timedelta(**max_timeout)

View File

@ -7,6 +7,7 @@ import re
import textwrap
import threading
from oslo_config import cfg
import pluggy
import aprsd
@ -15,6 +16,7 @@ from aprsd.packets import watch_list
# setup the global logger
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
CORE_MESSAGE_PLUGINS = [
@ -57,8 +59,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
# Set this in setup()
enabled = False
def __init__(self, config):
self.config = config
def __init__(self):
self.message_counter = 0
self.setup()
self.threads = self.create_threads() or []
@ -140,15 +141,10 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
def setup(self):
# if we have a watch list enabled, we need to add filtering
# to enable seeing packets from the watch list.
if "watch_list" in self.config["aprsd"] and self.config["aprsd"][
"watch_list"
].get("enabled", False):
if CONF.watch_list.enabled:
# watch list is enabled
self.enabled = True
watch_list = self.config["aprsd"]["watch_list"].get(
"callsigns",
[],
)
watch_list = CONF.watch_list.callsigns
# make sure the timeout is set or this doesn't work
if watch_list:
aprs_client = client.factory.create().client
@ -211,36 +207,40 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
@hookimpl
def filter(self, packet: packets.core.MessagePacket):
if not self.enabled:
result = f"{self.__class__.__name__} isn't enabled"
LOG.warning(result)
return result
if not isinstance(packet, packets.core.MessagePacket):
LOG.warning(f"Got a {packet.__class__.__name__} ignoring")
return packets.NULL_MESSAGE
result = None
message = packet.get("message_text", None)
msg_format = packet.get("format", None)
tocall = packet.get("addresse", None)
message = packet.message_text
tocall = packet.to_call
# Only process messages destined for us
# and is an APRS message format and has a message.
if (
tocall == self.config["aprs"]["login"]
and msg_format == "message"
tocall == CONF.callsign
and isinstance(packet, packets.core.MessagePacket)
and message
):
if re.search(self.command_regex, message):
self.rx_inc()
if self.enabled:
try:
result = self.process(packet)
except Exception as ex:
LOG.error(
"Plugin {} failed to process packet {}".format(
self.__class__, ex,
),
)
LOG.exception(ex)
if result:
self.tx_inc()
else:
result = f"{self.__class__.__name__} isn't enabled"
LOG.warning(result)
try:
result = self.process(packet)
except Exception as ex:
LOG.error(
"Plugin {} failed to process packet {}".format(
self.__class__, ex,
),
)
LOG.exception(ex)
if result:
self.tx_inc()
return result
@ -249,12 +249,11 @@ class APRSFIKEYMixin:
"""Mixin class to enable checking the existence of the aprs.fi apiKey."""
def ensure_aprs_fi_key(self):
try:
self.config.check_option(["services", "aprs.fi", "apiKey"])
self.enabled = True
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
if not CONF.aprs_fi.apiKey:
LOG.error("Config aprs_fi.apiKey is not set")
self.enabled = False
else:
self.enabled = True
class HelpPlugin(APRSDRegexCommandPluginBase):
@ -371,12 +370,17 @@ class PluginManager:
:param kwargs: parameters to pass
:return:
"""
module_name, class_name = module_class_string.rsplit(".", 1)
module_name = None
class_name = None
try:
module_name, class_name = module_class_string.rsplit(".", 1)
module = importlib.import_module(module_name)
module = importlib.reload(module)
except Exception as ex:
LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'")
if not module_name:
LOG.error(f"Failed to load Plugin {module_class_string}")
else:
LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'")
return
assert hasattr(module, class_name), "class {} is not in {}".format(
@ -406,25 +410,31 @@ class PluginManager:
plugin_obj = self._create_class(
plugin_name,
APRSDPluginBase,
config=self.config,
)
if plugin_obj:
if isinstance(plugin_obj, APRSDWatchListPluginBase):
LOG.info(
"Registering WatchList plugin '{}'({})".format(
plugin_name,
plugin_obj.version,
),
)
self._watchlist_pm.register(plugin_obj)
if plugin_obj.enabled:
LOG.info(
"Registering WatchList plugin '{}'({})".format(
plugin_name,
plugin_obj.version,
),
)
self._watchlist_pm.register(plugin_obj)
else:
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
else:
LOG.info(
"Registering plugin '{}'({})".format(
plugin_name,
plugin_obj.version,
),
)
self._pluggy_pm.register(plugin_obj)
if plugin_obj.enabled:
LOG.info(
"Registering plugin '{}'({}) -- {}".format(
plugin_name,
plugin_obj.version,
plugin_obj.command_regex,
),
)
self._pluggy_pm.register(plugin_obj)
else:
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
except Exception as ex:
LOG.error(f"Couldn't load plugin '{plugin_name}'")
LOG.exception(ex)
@ -440,10 +450,10 @@ class PluginManager:
LOG.info("Loading APRSD Plugins")
self._init()
# Help plugin is always enabled.
_help = HelpPlugin(self.config)
_help = HelpPlugin()
self._pluggy_pm.register(_help)
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
enabled_plugins = CONF.enabled_plugins
if enabled_plugins:
for p_name in enabled_plugins:
self._load_plugin(p_name)

View File

@ -25,14 +25,20 @@ def get_aprs_fi(api_key, callsign):
def get_weather_gov_for_gps(lat, lon):
# FIXME(hemna) This is currently BROKEN
LOG.debug(f"Fetch station at {lat}, {lon}")
headers = requests.utils.default_headers()
headers.update(
{"User-Agent": "(aprsd, waboring@hemna.com)"},
)
try:
url2 = (
"https://forecast.weather.gov/MapClick.php?lat=%s"
"&lon=%s&FcstType=json" % (lat, lon)
# "https://forecast.weather.gov/MapClick.php?lat=%s"
# "&lon=%s&FcstType=json" % (lat, lon)
f"https://api.weather.gov/points/{lat},{lon}"
)
LOG.debug(f"Fetching weather '{url2}'")
response = requests.get(url2)
response = requests.get(url2, headers=headers)
except Exception as e:
LOG.error(e)
raise Exception("Failed to get weather")

View File

@ -9,13 +9,16 @@ import threading
import time
import imapclient
from oslo_config import cfg
from aprsd import packets, plugin, stats, threads
from aprsd.threads import tx
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
shortcuts_dict = None
class EmailInfo:
@ -71,18 +74,18 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
def setup(self):
"""Ensure that email is enabled and start the thread."""
email_enabled = self.config["aprsd"]["email"].get("enabled", False)
if email_enabled:
if CONF.email_plugin.enabled:
self.enabled = True
shortcuts = _build_shortcuts_dict()
LOG.info(f"Email shortcuts {shortcuts}")
else:
LOG.info("Email services not enabled.")
self.enabled = False
def create_threads(self):
if self.enabled:
return APRSDEmailThread(
config=self.config,
)
return APRSDEmailThread()
@trace.trace
def process(self, packet: packets.MessagePacket):
@ -97,18 +100,18 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
ack = packet.get("msgNo", "0")
reply = None
if not self.config["aprsd"]["email"].get("enabled", False):
if not CONF.email_plugin.enabled:
LOG.debug("Email is not enabled in config file ignoring.")
return "Email not enabled."
searchstring = "^" + self.config["ham"]["callsign"] + ".*"
searchstring = "^" + CONF.email_plugin.callsign + ".*"
# only I can do email
if re.search(searchstring, fromcall):
# digits only, first one is number of emails to resend
r = re.search("^-([0-9])[0-9]*$", message)
if r is not None:
LOG.debug("RESEND EMAIL")
resend_email(self.config, r.group(1), fromcall)
resend_email(r.group(1), fromcall)
reply = packets.NULL_MESSAGE
# -user@address.com body of email
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
@ -118,7 +121,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
to_addr = a.group(1)
content = a.group(2)
email_address = get_email_from_shortcut(self.config, to_addr)
email_address = get_email_from_shortcut(to_addr)
if not email_address:
reply = "Bad email address"
return reply
@ -128,7 +131,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
content = (
"Click for my location: http://aprs.fi/{}" ""
).format(
self.config["ham"]["callsign"],
CONF.email_plugin.callsign,
)
too_soon = 0
now = time.time()
@ -141,7 +144,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
too_soon = 1
if not too_soon or ack == 0:
LOG.info(f"Send email '{content}'")
send_result = send_email(self.config, to_addr, content)
send_result = send_email(to_addr, content)
reply = packets.NULL_MESSAGE
if send_result != 0:
reply = f"-{to_addr} failed"
@ -169,9 +172,9 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
return reply
def _imap_connect(config):
imap_port = config["aprsd"]["email"]["imap"].get("port", 143)
use_ssl = config["aprsd"]["email"]["imap"].get("use_ssl", False)
def _imap_connect():
imap_port = CONF.email_plugin.imap_port
use_ssl = CONF.email_plugin.imap_use_ssl
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
# LOG.debug("Connect to IMAP host {} with user '{}'".
@ -179,7 +182,7 @@ def _imap_connect(config):
try:
server = imapclient.IMAPClient(
config["aprsd"]["email"]["imap"]["host"],
CONF.email_plugin.imap_host,
port=imap_port,
use_uid=True,
ssl=use_ssl,
@ -191,8 +194,8 @@ def _imap_connect(config):
try:
server.login(
config["aprsd"]["email"]["imap"]["login"],
config["aprsd"]["email"]["imap"]["password"],
CONF.email_plugin.imap_login,
CONF.email_plugin.imap_password,
)
except (imaplib.IMAP4.error, Exception) as e:
msg = getattr(e, "message", repr(e))
@ -208,15 +211,15 @@ def _imap_connect(config):
return server
def _smtp_connect(config):
host = config["aprsd"]["email"]["smtp"]["host"]
smtp_port = config["aprsd"]["email"]["smtp"]["port"]
use_ssl = config["aprsd"]["email"]["smtp"].get("use_ssl", False)
def _smtp_connect():
host = CONF.email_plugin.smtp_host
smtp_port = CONF.email_plugin.smtp_port
use_ssl = CONF.email_plugin.smtp_use_ssl
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
LOG.debug(
"Connect to SMTP host {} with user '{}'".format(
msg,
config["aprsd"]["email"]["imap"]["login"],
CONF.email_plugin.smtp_login,
),
)
@ -239,15 +242,15 @@ def _smtp_connect(config):
LOG.debug(f"Connected to smtp host {msg}")
debug = config["aprsd"]["email"]["smtp"].get("debug", False)
debug = CONF.email_plugin.debug
if debug:
server.set_debuglevel(5)
server.sendmail = trace.trace(server.sendmail)
try:
server.login(
config["aprsd"]["email"]["smtp"]["login"],
config["aprsd"]["email"]["smtp"]["password"],
CONF.email_plugin.smtp_login,
CONF.email_plugin.smtp_password,
)
except Exception:
LOG.error("Couldn't connect to SMTP Server")
@ -257,22 +260,39 @@ def _smtp_connect(config):
return server
def get_email_from_shortcut(config, addr):
if config["aprsd"]["email"].get("shortcuts", False):
return config["aprsd"]["email"]["shortcuts"].get(addr, addr)
def _build_shortcuts_dict():
global shortcuts_dict
if not shortcuts_dict:
if CONF.email_plugin.email_shortcuts:
shortcuts_dict = {}
tmp = CONF.email_plugin.email_shortcuts
for combo in tmp:
entry = combo.split("=")
shortcuts_dict[entry[0]] = entry[1]
else:
shortcuts_dict = {}
return shortcuts_dict
def get_email_from_shortcut(addr):
if CONF.email_plugin.email_shortcuts:
shortcuts = _build_shortcuts_dict()
LOG.info(f"Shortcut lookup {addr} returns {shortcuts.get(addr, addr)}")
return shortcuts.get(addr, addr)
else:
return addr
def validate_email_config(config, disable_validation=False):
def validate_email_config(disable_validation=False):
"""function to simply ensure we can connect to email services.
This helps with failing early during startup.
"""
LOG.info("Checking IMAP configuration")
imap_server = _imap_connect(config)
imap_server = _imap_connect()
LOG.info("Checking SMTP configuration")
smtp_server = _smtp_connect(config)
smtp_server = _smtp_connect()
if imap_server and smtp_server:
return True
@ -376,16 +396,16 @@ def parse_email(msgid, data, server):
@trace.trace
def send_email(config, to_addr, content):
shortcuts = config["aprsd"]["email"]["shortcuts"]
email_address = get_email_from_shortcut(config, to_addr)
def send_email(to_addr, content):
shortcuts = _build_shortcuts_dict()
email_address = get_email_from_shortcut(to_addr)
LOG.info("Sending Email_________________")
if to_addr in shortcuts:
LOG.info(f"To : {to_addr}")
to_addr = email_address
LOG.info(f" ({to_addr})")
subject = config["ham"]["callsign"]
subject = CONF.email_plugin.callsign
# content = content + "\n\n(NOTE: reply with one line)"
LOG.info(f"Subject : {subject}")
LOG.info(f"Body : {content}")
@ -395,13 +415,13 @@ def send_email(config, to_addr, content):
msg = MIMEText(content)
msg["Subject"] = subject
msg["From"] = config["aprsd"]["email"]["smtp"]["login"]
msg["From"] = CONF.email_plugin.smtp_login
msg["To"] = to_addr
server = _smtp_connect(config)
server = _smtp_connect()
if server:
try:
server.sendmail(
config["aprsd"]["email"]["smtp"]["login"],
CONF.email_plugin.smtp_login,
[to_addr],
msg.as_string(),
)
@ -415,19 +435,19 @@ def send_email(config, to_addr, content):
@trace.trace
def resend_email(config, count, fromcall):
def resend_email(count, fromcall):
date = datetime.datetime.now()
month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day
year = date.year
today = f"{day}-{month}-{year}"
shortcuts = config["aprsd"]["email"]["shortcuts"]
shortcuts = _build_shortcuts_dict()
# swap key/value
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
try:
server = _imap_connect(config)
server = _imap_connect()
except Exception:
LOG.exception("Failed to Connect to IMAP. Cannot resend email ")
return
@ -467,7 +487,7 @@ def resend_email(config, count, fromcall):
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
tx.send(
packets.MessagePacket(
from_call=config["aprsd"]["callsign"],
from_call=CONF.callsign,
to_call=fromcall,
message_text=reply,
),
@ -490,7 +510,7 @@ def resend_email(config, count, fromcall):
)
tx.send(
packets.MessagePacket(
from_call=config["aprsd"]["callsign"],
from_call=CONF.callsign,
to_call=fromcall,
message_text=reply,
),
@ -504,9 +524,8 @@ def resend_email(config, count, fromcall):
class APRSDEmailThread(threads.APRSDThread):
def __init__(self, config):
def __init__(self):
super().__init__("EmailThread")
self.config = config
self.past = datetime.datetime.now()
def loop(self):
@ -527,7 +546,7 @@ class APRSDEmailThread(threads.APRSDThread):
f"check_email_delay is {EmailInfo().delay} seconds ",
)
shortcuts = self.config["aprsd"]["email"]["shortcuts"]
shortcuts = _build_shortcuts_dict()
# swap key/value
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
@ -538,7 +557,7 @@ class APRSDEmailThread(threads.APRSDThread):
today = f"{day}-{month}-{year}"
try:
server = _imap_connect(self.config)
server = _imap_connect()
except Exception:
LOG.exception("IMAP Failed to connect")
return True
@ -611,8 +630,8 @@ class APRSDEmailThread(threads.APRSDThread):
# config ham.callsign
tx.send(
packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"],
to_call=self.config["ham"]["callsign"],
from_call=CONF.callsign,
to_call=CONF.email_plugin.callsign,
message_text=reply,
),
)

View File

@ -2,10 +2,13 @@ import logging
import re
import time
from oslo_config import cfg
from aprsd import packets, plugin, plugin_utils
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -24,9 +27,8 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
LOG.info("Location Plugin")
fromcall = packet.from_call
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
api_key = self.config["services"]["aprs.fi"]["apiKey"]
api_key = CONF.aprs_fi.apiKey
# optional second argument is a callsign to search
a = re.search(r"^.*\s+(.*)", message)

View File

@ -1,8 +1,11 @@
import logging
from oslo_config import cfg
from aprsd import packets, plugin
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -20,7 +23,7 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
def process(self, packet: packets.MessagePacket):
LOG.info("NotifySeenPlugin")
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"]
notify_callsign = CONF.watch_list.alert_callsign
fromcall = packet.from_call
wl = packets.WatchList()
@ -38,7 +41,7 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
packet_type = packet.__class__.__name__
# we shouldn't notify the alert user that they are online.
pkt = packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"],
from_call=CONF.callsign,
to_call=notify_callsign,
message_text=(
f"{fromcall} was just seen by type:'{packet_type}'"

View File

@ -2,11 +2,14 @@ import datetime
import logging
import re
from oslo_config import cfg
from aprsd import packets, plugin
from aprsd.packets import tracker
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -17,13 +20,19 @@ class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
command_name = "query"
short_description = "APRSD Owner command to query messages in the MsgTrack"
def setup(self):
"""Do any plugin setup here."""
if not CONF.query_plugin.callsign:
LOG.error("Config query_plugin.callsign not set. Disabling plugin")
self.enabled = False
self.enabled = True
@trace.trace
def process(self, packet: packets.MessagePacket):
LOG.info("Query COMMAND")
fromcall = packet.from_call
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
pkt_tracker = tracker.PacketTrack()
now = datetime.datetime.now()
@ -32,7 +41,7 @@ class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
now.strftime("%H:%M:%S"),
)
searchstring = "^" + self.config["ham"]["callsign"] + ".*"
searchstring = "^" + CONF.query_plugin.callsign + ".*"
# only I can do admin commands
if re.search(searchstring, fromcall):

View File

@ -2,12 +2,14 @@ import logging
import re
import time
from oslo_config import cfg
import pytz
from aprsd import packets, plugin, plugin_utils
from aprsd.utils import fuzzy, trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -74,7 +76,7 @@ class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin):
# if no second argument, search for calling station
searchcall = fromcall
api_key = self.config["services"]["aprs.fi"]["apiKey"]
api_key = CONF.aprs_fi.apiKey
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:

View File

@ -2,7 +2,6 @@ import logging
import aprsd
from aprsd import plugin, stats
from aprsd.utils import trace
LOG = logging.getLogger("APRSD")
@ -19,7 +18,6 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
# five mins {int:int}
email_sent_dict = {}
@trace.trace
def process(self, packet):
LOG.info("Version COMMAND")
# fromcall = packet.get("from")
@ -27,6 +25,7 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
# ack = packet.get("msgNo", "0")
stats_obj = stats.APRSDStats()
s = stats_obj.stats()
print(s)
return "APRSD ver:{} uptime:{}".format(
aprsd.__version__,
s["aprsd"]["uptime"],

View File

@ -2,12 +2,14 @@ import json
import logging
import re
from oslo_config import cfg
import requests
from aprsd import plugin, plugin_utils
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -34,10 +36,10 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
@trace.trace
def process(self, packet):
LOG.info("Weather Plugin")
fromcall = packet.get("from")
fromcall = packet.from_call
# message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
api_key = self.config["services"]["aprs.fi"]["apiKey"]
api_key = CONF.aprs_fi.apiKey
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
except Exception as ex:
@ -58,6 +60,8 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
return "Unable to get weather"
LOG.info(f"WX data {wx_data}")
reply = (
"%sF(%sF/%sF) %s. %s, %s."
% (
@ -97,6 +101,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
@trace.trace
def process(self, packet):
print("FISTY")
fromcall = packet.get("from")
message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
@ -119,13 +124,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
# if no second argument, search for calling station
fromcall = fromcall
try:
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
api_key = self.config["services"]["aprs.fi"]["apiKey"]
api_key = CONF.aprs_fi.apiKey
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
@ -187,6 +186,13 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
command_name = "OpenWeatherMap"
short_description = "OpenWeatherMap weather of GPS Beacon location"
def setup(self):
if not CONF.owm_weather_plugin.apiKey:
LOG.error("Config.owm_weather_plugin.apiKey is not set. Disabling")
self.enabled = False
else:
self.enabled = True
def help(self):
_help = [
"openweathermap: Send {} to get weather "
@ -209,13 +215,8 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
else:
searchcall = fromcall
try:
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
api_key = CONF.aprs_fi.apiKey
api_key = self.config["services"]["aprs.fi"]["apiKey"]
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
@ -230,21 +231,8 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
try:
self.config.exists(["services", "openweathermap", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config openweathermap:apiKey {ex}")
return "No openweathermap apiKey found"
try:
self.config.exists(["aprsd", "units"])
except Exception:
LOG.debug("Couldn't find untis in aprsd:services:units")
units = "metric"
else:
units = self.config["aprsd"]["units"]
api_key = self.config["services"]["openweathermap"]["apiKey"]
units = CONF.units
api_key = CONF.owm_weather_plugin.apiKey
try:
wx_data = plugin_utils.fetch_openweathermap(
api_key,
@ -317,6 +305,16 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
command_name = "AVWXWeather"
short_description = "AVWX weather of GPS Beacon location"
def setup(self):
if not CONF.avwx_plugin.base_url:
LOG.error("Config avwx_plugin.base_url not specified. Disabling")
return False
elif not CONF.avwx_plugin.apiKey:
LOG.error("Config avwx_plugin.apiKey not specified. Disabling")
return False
else:
return True
def help(self):
_help = [
"avwxweather: Send {} to get weather "
@ -339,13 +337,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
else:
searchcall = fromcall
try:
self.config.exists(["services", "aprs.fi", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
return "No aprs.fi apikey found"
api_key = self.config["services"]["aprs.fi"]["apiKey"]
api_key = CONF.aprs_fi.apiKey
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
@ -360,21 +352,8 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
try:
self.config.exists(["services", "avwx", "apiKey"])
except Exception as ex:
LOG.error(f"Failed to find config avwx:apiKey {ex}")
return "No avwx apiKey found"
try:
self.config.exists(self.config, ["services", "avwx", "base_url"])
except Exception as ex:
LOG.debug(f"Didn't find avwx:base_url {ex}")
base_url = "https://avwx.rest"
else:
base_url = self.config["services"]["avwx"]["base_url"]
api_key = self.config["services"]["avwx"]["apiKey"]
api_key = CONF.avwx_plugin.apiKey
base_url = CONF.avwx_plugin.base_url
token = f"TOKEN {api_key}"
headers = {"Authorization": token}
try:

90
aprsd/rpc_server.py Normal file
View File

@ -0,0 +1,90 @@
import json
import logging
from oslo_config import cfg
import rpyc
from rpyc.utils.authenticators import AuthenticationError
from rpyc.utils.server import ThreadPoolServer
from aprsd import conf # noqa: F401
from aprsd import packets, stats, threads
from aprsd.threads import log_monitor
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
def magic_word_authenticator(sock):
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
if magic != CONF.rpc_settings.magic_word:
raise AuthenticationError(f"wrong magic word {magic}")
return sock, None
class APRSDRPCThread(threads.APRSDThread):
def __init__(self):
super().__init__(name="RPCThread")
self.thread = ThreadPoolServer(
APRSDService,
port=CONF.rpc_settings.port,
protocol_config={"allow_public_attrs": True},
authenticator=magic_word_authenticator,
)
def stop(self):
if self.thread:
self.thread.close()
self.thread_stop = True
def loop(self):
# there is no loop as run is blocked
if self.thread and not self.thread_stop:
# This is a blocking call
self.thread.start()
@rpyc.service
class APRSDService(rpyc.Service):
def on_connect(self, conn):
# code that runs when a connection is created
# (to init the service, if needed)
LOG.info("Connected")
self._conn = conn
def on_disconnect(self, conn):
# code that runs after the connection has already closed
# (to finalize the service, if needed)
LOG.info("Disconnected")
self._conn = None
@rpyc.exposed
def get_stats(self):
stat = stats.APRSDStats()
stats_dict = stat.stats()
return json.dumps(stats_dict, indent=4, sort_keys=True, default=str)
@rpyc.exposed
def get_stats_obj(self):
return stats.APRSDStats()
@rpyc.exposed
def get_packet_list(self):
return packets.PacketList()
@rpyc.exposed
def get_packet_track(self):
return packets.PacketTrack()
@rpyc.exposed
def get_watch_list(self):
return packets.WatchList()
@rpyc.exposed
def get_seen_list(self):
return packets.SeenList()
@rpyc.exposed
def get_log_entries(self):
entries = log_monitor.LogEntries().get_all_and_purge()
return json.dumps(entries, default=str)

View File

@ -2,12 +2,14 @@ import datetime
import logging
import threading
from oslo_config import cfg
import wrapt
import aprsd
from aprsd import packets, plugin, utils
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -15,7 +17,6 @@ class APRSDStats:
_instance = None
lock = threading.Lock()
config = None
start_time = None
_aprsis_server = None
@ -62,15 +63,11 @@ class APRSDStats:
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
# any initializetion here
# any init here
cls._instance.start_time = datetime.datetime.now()
cls._instance._aprsis_keepalive = datetime.datetime.now()
return cls._instance
def __init__(self, config=None):
if config:
self.config = config
@wrapt.synchronized(lock)
@property
def uptime(self):
@ -191,7 +188,7 @@ class APRSDStats:
"aprsd": {
"version": aprsd.__version__,
"uptime": utils.strfdelta(self.uptime),
"callsign": self.config["aprsd"]["callsign"],
"callsign": CONF.callsign,
"memory_current": int(self.memory),
"memory_current_str": utils.human_size(self.memory),
"memory_peak": int(self.memory_peak),
@ -201,7 +198,7 @@ class APRSDStats:
},
"aprs-is": {
"server": str(self.aprsis_server),
"callsign": self.config["aprs"]["login"],
"callsign": CONF.aprs_network.login,
"last_update": last_aprsis_keepalive,
},
"packets": {
@ -215,7 +212,7 @@ class APRSDStats:
"ack_sent": self._pkt_cnt["AckPacket"]["tx"],
},
"email": {
"enabled": self.config["aprsd"]["email"]["enabled"],
"enabled": CONF.email_plugin.enabled,
"sent": int(self._email_tx),
"received": int(self._email_rx),
"thread_last_update": last_update,

View File

@ -3,10 +3,13 @@ import logging
import time
import tracemalloc
from oslo_config import cfg
from aprsd import client, packets, stats, utils
from aprsd.threads import APRSDThread, APRSDThreadList
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@ -14,10 +17,9 @@ class KeepAliveThread(APRSDThread):
cntr = 0
checker_time = datetime.datetime.now()
def __init__(self, config):
def __init__(self):
tracemalloc.start()
super().__init__("KeepAlive")
self.config = config
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
self.max_delta = datetime.timedelta(**max_timeout)
@ -40,15 +42,9 @@ class KeepAliveThread(APRSDThread):
stats_obj.set_memory(current)
stats_obj.set_memory_peak(peak)
try:
login = self.config["aprsd"]["callsign"]
except KeyError:
login = self.config["ham"]["callsign"]
login = CONF.callsign
if pkt_tracker.is_initialized():
tracked_packets = len(pkt_tracker)
else:
tracked_packets = 0
tracked_packets = len(pkt_tracker)
keepalive = (
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
@ -77,7 +73,7 @@ class KeepAliveThread(APRSDThread):
if delta > self.max_delta:
# We haven't gotten a keepalive from aprs-is in a while
# reset the connection.a
if not client.KISSClient.is_enabled(self.config):
if not client.KISSClient.is_enabled():
LOG.warning(f"Resetting connection to APRS-IS {delta}")
client.factory.create().reset()

View File

@ -0,0 +1,77 @@
import logging
import threading
import wrapt
from aprsd import threads
from aprsd.logging import log
LOG = logging.getLogger("APRSD")
class LogEntries:
entries = []
lock = threading.Lock()
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@wrapt.synchronized(lock)
def add(self, entry):
self.entries.append(entry)
@wrapt.synchronized(lock)
def get_all_and_purge(self):
entries = self.entries.copy()
self.entries = []
return entries
@wrapt.synchronized(lock)
def __len__(self):
return len(self.entries)
class LogMonitorThread(threads.APRSDThread):
def __init__(self):
super().__init__("LogMonitorThread")
def loop(self):
try:
record = log.logging_queue.get(block=True, timeout=2)
if isinstance(record, list):
for item in record:
entry = self.json_record(item)
LogEntries().add(entry)
else:
entry = self.json_record(record)
LogEntries().add(entry)
except Exception:
# Just ignore thi
pass
return True
def json_record(self, record):
entry = {}
entry["filename"] = record.filename
entry["funcName"] = record.funcName
entry["levelname"] = record.levelname
entry["lineno"] = record.lineno
entry["module"] = record.module
entry["name"] = record.name
entry["pathname"] = record.pathname
entry["process"] = record.process
entry["processName"] = record.processName
if hasattr(record, "stack_info"):
entry["stack_info"] = record.stack_info
else:
entry["stack_info"] = None
entry["thread"] = record.thread
entry["threadName"] = record.threadName
entry["message"] = record.getMessage()
return entry

View File

@ -4,18 +4,19 @@ import queue
import time
import aprslib
from oslo_config import cfg
from aprsd import client, packets, plugin
from aprsd.threads import APRSDThread, tx
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
class APRSDRXThread(APRSDThread):
def __init__(self, config, packet_queue):
def __init__(self, packet_queue):
super().__init__("RX_MSG")
self.config = config
self.packet_queue = packet_queue
self._client = client.factory.create()
@ -80,8 +81,7 @@ class APRSDProcessPacketThread(APRSDThread):
will ack a message before sending the packet to the subclass
for processing."""
def __init__(self, config, packet_queue):
self.config = config
def __init__(self, packet_queue):
self.packet_queue = packet_queue
super().__init__("ProcessPKT")
self._loop_cnt = 1
@ -106,7 +106,7 @@ class APRSDProcessPacketThread(APRSDThread):
def process_packet(self, packet):
"""Process a packet received from aprs-is server."""
LOG.debug(f"RXPKT-LOOP {self._loop_cnt}")
our_call = self.config["aprsd"]["callsign"].lower()
our_call = CONF.callsign.lower()
from_call = packet.from_call
if packet.addresse:
@ -133,7 +133,7 @@ class APRSDProcessPacketThread(APRSDThread):
# send an ack last
tx.send(
packets.AckPacket(
from_call=self.config["aprsd"]["callsign"],
from_call=CONF.callsign,
to_call=from_call,
msgNo=msg_id,
),
@ -178,11 +178,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
if isinstance(subreply, packets.Packet):
tx.send(subreply)
else:
wl = self.config["aprsd"]["watch_list"]
wl = CONF.watch_list
to_call = wl["alert_callsign"]
tx.send(
packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"],
from_call=CONF.callsign,
to_call=to_call,
message_text=subreply,
),
@ -219,7 +219,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
else:
tx.send(
packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"],
from_call=CONF.callsign,
to_call=from_call,
message_text=subreply,
),
@ -238,7 +238,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
LOG.debug(f"Sending '{reply}'")
tx.send(
packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"],
from_call=CONF.callsign,
to_call=from_call,
message_text=reply,
),
@ -246,12 +246,12 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
# If the message was for us and we didn't have a
# response, then we send a usage statement.
if to_call == self.config["aprsd"]["callsign"] and not replied:
if to_call == CONF.callsign and not replied:
LOG.warning("Sending help!")
message_text = "Unknown command! Send 'help' message for help"
tx.send(
packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"],
from_call=CONF.callsign,
to_call=from_call,
message_text=message_text,
),
@ -260,11 +260,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
LOG.error("Plugin failed!!!")
LOG.exception(ex)
# Do we need to send a reply?
if to_call == self.config["aprsd"]["callsign"]:
if to_call == CONF.callsign:
reply = "A Plugin failed! try again?"
tx.send(
packets.MessagePacket(
from_call=self.config["aprsd"]["callsign"],
from_call=CONF.callsign,
to_call=from_call,
message_text=reply,
),

View File

@ -4,7 +4,7 @@ import time
from aprsd import client
from aprsd import threads as aprsd_threads
from aprsd.packets import core, packet_list, tracker
from aprsd.packets import core, tracker
LOG = logging.getLogger("APRSD")
@ -27,9 +27,9 @@ def send(packet: core.Packet, direct=False, aprs_client=None):
else:
cl = client.factory.create()
packet.update_timestamp()
packet.log(header="TX")
cl.send(packet)
packet_list.PacketList().tx(packet)
class SendPacketThread(aprsd_threads.APRSDThread):
@ -94,8 +94,8 @@ class SendPacketThread(aprsd_threads.APRSDThread):
if send_now:
# no attempt time, so lets send it, and start
# tracking the time.
send(packet, direct=True)
packet.last_send_time = datetime.datetime.now()
send(packet, direct=True)
packet.send_count += 1
time.sleep(1)

View File

@ -1,16 +1,16 @@
import abc
import logging
import os
import pathlib
import pickle
from aprsd import config as aprsd_config
from oslo_config import cfg
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
class ObjectStoreMixin(metaclass=abc.ABCMeta):
class ObjectStoreMixin:
"""Class 'MIXIN' intended to save/load object data.
The asumption of how this mixin is used:
@ -24,13 +24,6 @@ class ObjectStoreMixin(metaclass=abc.ABCMeta):
When APRSD Starts, it calls load()
aprsd server -f (flush) will wipe all saved objects.
"""
@abc.abstractmethod
def is_initialized(self):
"""Return True if the class has been setup correctly.
If this returns False, the ObjectStore doesn't save anything.
"""
def __len__(self):
return len(self.data)
@ -44,25 +37,18 @@ class ObjectStoreMixin(metaclass=abc.ABCMeta):
return self.data[id]
def _init_store(self):
if self.is_initialized():
sl = self._save_location()
if not os.path.exists(sl):
LOG.warning(f"Save location {sl} doesn't exist")
try:
os.makedirs(sl)
except Exception as ex:
LOG.exception(ex)
else:
LOG.warning(f"{self.__class__.__name__} is not initialized")
def _save_location(self):
save_location = self.config.get("aprsd.save_location", None)
if not save_location:
save_location = aprsd_config.DEFAULT_CONFIG_DIR
return save_location
if not CONF.enable_save:
return
sl = CONF.save_location
if not os.path.exists(sl):
LOG.warning(f"Save location {sl} doesn't exist")
try:
os.makedirs(sl)
except Exception as ex:
LOG.exception(ex)
def _save_filename(self):
save_location = self._save_location()
save_location = CONF.save_location
return "{}/{}.p".format(
save_location,
@ -79,45 +65,48 @@ class ObjectStoreMixin(metaclass=abc.ABCMeta):
def save(self):
"""Save any queued to disk?"""
if self.is_initialized():
if len(self) > 0:
LOG.info(
f"{self.__class__.__name__}::Saving"
f" {len(self)} entries to disk at"
f"{self._save_location()}",
)
with open(self._save_filename(), "wb+") as fp:
pickle.dump(self._dump(), fp)
else:
LOG.debug(
"{} Nothing to save, flushing old save file '{}'".format(
self.__class__.__name__,
self._save_filename(),
),
)
self.flush()
if not CONF.enable_save:
return
if len(self) > 0:
LOG.info(
f"{self.__class__.__name__}::Saving"
f" {len(self)} entries to disk at"
f"{CONF.save_location}",
)
with open(self._save_filename(), "wb+") as fp:
pickle.dump(self._dump(), fp)
else:
LOG.debug(
"{} Nothing to save, flushing old save file '{}'".format(
self.__class__.__name__,
self._save_filename(),
),
)
self.flush()
def load(self):
if self.is_initialized():
if os.path.exists(self._save_filename()):
try:
with open(self._save_filename(), "rb") as fp:
raw = pickle.load(fp)
if raw:
self.data = raw
LOG.debug(
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
)
LOG.debug(f"{self.data}")
except (pickle.UnpicklingError, Exception) as ex:
LOG.error(f"Failed to UnPickle {self._save_filename()}")
LOG.error(ex)
self.data = {}
if not CONF.enable_save:
return
if os.path.exists(self._save_filename()):
try:
with open(self._save_filename(), "rb") as fp:
raw = pickle.load(fp)
if raw:
self.data = raw
LOG.debug(
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
)
LOG.debug(f"{self.data}")
except (pickle.UnpicklingError, Exception) as ex:
LOG.error(f"Failed to UnPickle {self._save_filename()}")
LOG.error(ex)
self.data = {}
def flush(self):
"""Nuke the old pickle file that stored the old results from last aprsd run."""
if self.is_initialized():
if os.path.exists(self._save_filename()):
pathlib.Path(self._save_filename()).unlink()
with self.lock:
self.data = {}
if not CONF.enable_save:
return
if os.path.exists(self._save_filename()):
pathlib.Path(self._save_filename()).unlink()
with self.lock:
self.data = {}

View File

@ -109,16 +109,17 @@ function update_packets( data ) {
}
jQuery.each(data, function(i, val) {
pkt = JSON.parse(val);
update_watchlist_from_packet(pkt['from_call'], pkt);
if ( packet_list.hasOwnProperty(val["timestamp"]) == false ) {
if ( packet_list.hasOwnProperty(pkt.timestamp) == false ) {
// Store the packet
packet_list[pkt["timestamp"]] = pkt;
packet_list[pkt.timestamp] = pkt;
//ts_str = val["timestamp"].toString();
//ts = ts_str.split(".")[0]*1000;
ts = pkt["timestamp"]
ts = pkt.timestamp
var d = new Date(ts).toLocaleDateString("en-US");
var t = new Date(ts).toLocaleTimeString("en-US");
var from_call = pkt['from_call'];
var from_call = pkt.from_call;
if (from_call == our_callsign) {
title_id = 'title_tx';
} else {

View File

@ -28,36 +28,6 @@ function init_messages() {
update_msg(msg);
});
$("#sendform").submit(function(event) {
event.preventDefault();
var $checkboxes = $(this).find('input[type=checkbox]');
//loop through the checkboxes and change to hidden fields
$checkboxes.each(function() {
if ($(this)[0].checked) {
$(this).attr('type', 'hidden');
$(this).val(1);
} else {
$(this).attr('type', 'hidden');
$(this).val(0);
}
});
msg = {'from': $('#from').val(),
'password': $('#password').val(),
'to': $('#to').val(),
'message': $('#message').val(),
'wait_reply': $('#wait_reply').val(),
}
socket.emit("send", msg);
//loop through the checkboxes and change to hidden fields
$checkboxes.each(function() {
$(this).attr('type', 'checkbox');
});
});
}
function add_msg(msg) {

View File

@ -82,7 +82,6 @@
<div class="item" data-tab="watch-tab">Watch List</div>
<div class="item" data-tab="plugin-tab">Plugins</div>
<div class="item" data-tab="config-tab">Config</div>
<div class="item" data-tab="send-tab">Send Message</div>
<div class="item" data-tab="log-tab">LogFile</div>
<div class="item" data-tab="raw-tab">Raw JSON</div>
</div>
@ -160,29 +159,6 @@
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
</div>
<div class="ui bottom attached tab segment" data-tab="send-tab">
<h3 class="ui dividing header">Send Message</h3>
<div id="sendMsgDiv" class="ui mini text">
<form id="sendform" name="sendmsg" action="">
<p><label for="from_call">From Callsign:</label>
<input type="text" name="from_call" id="from"></p>
<p><label for="from_call_password">Password:</label>
<input type="password" name="from_call_password" id='password'></p>
<p><label for="to_call">To Callsign:</label>
<input type="text" name="to_call" id="to" ></p>
<p><label for="message">Message:</label>
<input type="text" name="message" id="message" ></p>
<p><label for="wait">Wait for Reply?</label>
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
</p>
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
</form>
<div class="ui styled fluid accordion" id="accordion">
<div id="sendMsgsDiv" class="ui mini text">Messages</div>
</div>
</div>
</div>
<div class="ui bottom attached tab segment" data-tab="log-tab">
<h3 class="ui dividing header">LOGFILE</h3>
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>

View File

@ -1,15 +0,0 @@
<html>
<head>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="json-viewer/jquery.json-viewer.js"></script>
<link href="json-viewer/jquery.json-viewer.css" type="text/css" rel="stylesheet" />
</head>
<pre id="json-viewer"></pre>
<script>
var data = {{ messages | safe }}
$('#json-viewer').jsonViewer(data)
</script>
</html>

View File

@ -1,74 +0,0 @@
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-simple-websocket@1.1.4/src/jquery.simple.websocket.min.js"></script>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/components/prism-json.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.23.0/themes/prism-tomorrow.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
<link rel="stylesheet" href="/static/css/index.css">
<link rel="stylesheet" href="/static/css/tabs.css">
<script src="/static/js/send-message.js"></script>
<script language="JavaScript">
$(document).ready(function() {
init_messages();
});
</script>
</head>
<body>
<div class='ui text container'>
<h1 class='ui dividing header'>APRSD {{ version }}</h1>
</div>
<div class='ui grid text container'>
<div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span>
connected to
<span style='color: blue' id='aprsis'>NONE</span>
</div>
<div class='right floated four wide column'>
<span id='uptime'>NONE</span>
</div>
</div>
<h3 class="ui dividing header">Send Message Form</h3>
<form id="sendform" name="sendmsg" action="">
<p><label for="from_call">From Callsign:</label>
<input type="text" name="from_call" id="from" value="WB4BOR"></p>
<p><label for="from_call_password">Password:</label>
<input type="password" name="from_call_password" id='password' value="24496"></p>
<p><label for="to_call">To Callsign:</label>
<input type="text" name="to_call" id="to" value="WB4BOR-11"></p>
<p><label for="message">Message:</label>
<input type="text" name="message" id="message" value="ping"></p>
<p><label for="wait">Wait for Reply?</label>
<input type="checkbox" name="wait_reply" id="wait_reply" value="off" checked>
</p>
<input type="submit" name="submit" class="button" id="send_msg" value="Send" />
</form>
<h3 class="ui dividing header">Messages (<span id="msgs_count">0</span>)</h3>
<div class="ui styled fluid accordion" id="accordion">
<div id="msgsDiv" class="ui mini text">Messages</div>
</div>
</body>
</html>

View File

@ -1,12 +1,12 @@
#
# This file is autogenerated by pip-compile with Python 3.9
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --annotation-style=line --resolver=backtracking dev-requirements.in
#
add-trailing-comma==2.4.0 # via gray
alabaster==0.7.12 # via sphinx
attrs==22.1.0 # via jsonschema, pytest
attrs==22.2.0 # via jsonschema, pytest
autoflake==1.5.3 # via gray
babel==2.11.0 # via sphinx
black==22.12.0 # via gray
@ -20,21 +20,20 @@ click==8.1.3 # via black, pip-tools
colorama==0.4.6 # via tox
commonmark==0.9.1 # via rich
configargparse==1.5.3 # via gray
coverage[toml]==6.5.0 # via pytest-cov
coverage[toml]==7.0.1 # via pytest-cov
distlib==0.3.6 # via virtualenv
docutils==0.19 # via sphinx
exceptiongroup==1.0.4 # via pytest
exceptiongroup==1.1.0 # via pytest
filelock==3.8.2 # via tox, virtualenv
fixit==0.1.4 # via gray
flake8==6.0.0 # via -r dev-requirements.in, fixit, pep8-naming
gray==0.13.0 # via -r dev-requirements.in
identify==2.5.9 # via pre-commit
identify==2.5.11 # via pre-commit
idna==3.4 # via requests
imagesize==1.4.1 # via sphinx
importlib-metadata==5.1.0 # via sphinx
importlib-resources==5.10.1 # via fixit
iniconfig==1.1.1 # via pytest
isort==5.11.2 # via -r dev-requirements.in, gray
isort==5.11.4 # via -r dev-requirements.in, gray
jinja2==3.1.2 # via sphinx
jsonschema==4.17.3 # via fixit
libcst==0.4.9 # via fixit
@ -46,11 +45,11 @@ nodeenv==1.7.0 # via pre-commit
packaging==22.0 # via build, pyproject-api, pytest, sphinx, tox
pathspec==0.10.3 # via black
pep517==0.13.0 # via build
pep8-naming==0.13.2 # via -r dev-requirements.in
pip-tools==6.12.0 # via -r dev-requirements.in
pep8-naming==0.13.3 # via -r dev-requirements.in
pip-tools==6.12.1 # via -r dev-requirements.in
platformdirs==2.6.0 # via black, tox, virtualenv
pluggy==1.0.0 # via pytest, tox
pre-commit==2.20.0 # via -r dev-requirements.in
pre-commit==2.21.0 # via -r dev-requirements.in
pycodestyle==2.10.0 # via flake8
pyflakes==3.0.1 # via autoflake, flake8
pygments==2.13.0 # via rich, sphinx
@ -58,7 +57,7 @@ pyproject-api==1.2.1 # via tox
pyrsistent==0.19.2 # via jsonschema
pytest==7.2.0 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.0.0 # via -r dev-requirements.in
pytz==2022.6 # via babel
pytz==2022.7 # via babel
pyupgrade==3.3.1 # via gray
pyyaml==6.0 # via fixit, libcst, pre-commit
requests==2.28.1 # via sphinx
@ -72,17 +71,16 @@ sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==1.0.3 # via sphinx
sphinxcontrib-serializinghtml==1.1.5 # via sphinx
tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake, pre-commit
toml==0.10.2 # via autoflake
tomli==2.0.1 # via black, build, coverage, mypy, pep517, pyproject-api, pytest, tox
tox==4.0.9 # via -r dev-requirements.in
typing-extensions==4.4.0 # via black, libcst, mypy, typing-inspect
tox==4.0.18 # via -r dev-requirements.in
typing-extensions==4.4.0 # via libcst, mypy, typing-inspect
typing-inspect==0.8.0 # via libcst
unify==0.5 # via gray
untokenize==0.1.1 # via unify
urllib3==1.26.13 # via requests
virtualenv==20.17.1 # via pre-commit, tox
wheel==0.38.4 # via pip-tools
zipp==3.11.0 # via importlib-metadata, importlib-resources
# The following packages are considered to be unsafe in a requirements file:
# pip

View File

@ -41,7 +41,7 @@ RUN pip install aprsd==$APRSD_PIP_VERSION
# Ensure /config is there with a default config file
USER root
RUN mkdir -p /config
RUN aprsd sample-config > /config/aprsd.yml
RUN aprsd sample-config > /config/aprsd.conf
RUN chown -R $APRS_USER:$APRS_USER /config
# override this to run another configuration
@ -53,4 +53,4 @@ ADD bin/run.sh /usr/local/bin
ENTRYPOINT ["/usr/local/bin/run.sh"]
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
CMD aprsd healthcheck --config /config/aprsd.yml --url http://localhost:8001/stats
CMD aprsd healthcheck --config /config/aprsd.conf --url http://localhost:8001/stats

View File

@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM ubuntu:focal as aprsd
FROM ubuntu:22.04
# Dockerfile for building a container during aprsd development.
ARG BRANCH=master
@ -55,7 +55,7 @@ RUN ls -al /usr/local/bin
RUN ls -al /usr/bin
RUN which aprsd
RUN mkdir -p /config
RUN aprsd sample-config > /config/aprsd.yml
RUN aprsd sample-config > /config/aprsd.conf
RUN chown -R $APRS_USER:$APRS_USER /config
# override this to run another configuration
@ -67,4 +67,4 @@ ADD bin/run.sh $HOME/
ENTRYPOINT ["/home/aprs/run.sh"]
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
CMD aprsd healthcheck --config /config/aprsd.yml --url http://localhost:8001/stats
CMD aprsd healthcheck --config /config/aprsd.conf --url http://localhost:8001/stats

View File

@ -52,14 +52,6 @@ aprsd.plugins.query module
:undoc-members:
:show-inheritance:
aprsd.plugins.stock module
--------------------------
.. automodule:: aprsd.plugins.stock
:members:
:undoc-members:
:show-inheritance:
aprsd.plugins.time module
-------------------------

View File

@ -7,11 +7,35 @@ Subpackages
.. toctree::
:maxdepth: 4
aprsd.clients
aprsd.cmds
aprsd.conf
aprsd.logging
aprsd.packets
aprsd.plugins
aprsd.threads
aprsd.utils
aprsd.web
Submodules
----------
aprsd.aprsd module
------------------
.. automodule:: aprsd.aprsd
:members:
:undoc-members:
:show-inheritance:
aprsd.cli\_helper module
------------------------
.. automodule:: aprsd.cli_helper
:members:
:undoc-members:
:show-inheritance:
aprsd.client module
-------------------
@ -20,18 +44,10 @@ aprsd.client module
:undoc-members:
:show-inheritance:
aprsd.dev module
----------------
aprsd.exception module
----------------------
.. automodule:: aprsd.dev
:members:
:undoc-members:
:show-inheritance:
aprsd.fake\_aprs module
-----------------------
.. automodule:: aprsd.fake_aprs
.. automodule:: aprsd.exception
:members:
:undoc-members:
:show-inheritance:
@ -44,46 +60,6 @@ aprsd.flask module
:undoc-members:
:show-inheritance:
aprsd.fuzzyclock module
-----------------------
.. automodule:: aprsd.fuzzyclock
:members:
:undoc-members:
:show-inheritance:
aprsd.healthcheck module
------------------------
.. automodule:: aprsd.healthcheck
:members:
:undoc-members:
:show-inheritance:
aprsd.kissclient module
-----------------------
.. automodule:: aprsd.kissclient
:members:
:undoc-members:
:show-inheritance:
aprsd.listen module
-------------------
.. automodule:: aprsd.listen
:members:
:undoc-members:
:show-inheritance:
aprsd.main module
-----------------
.. automodule:: aprsd.main
:members:
:undoc-members:
:show-inheritance:
aprsd.messaging module
----------------------
@ -92,14 +68,6 @@ aprsd.messaging module
:undoc-members:
:show-inheritance:
aprsd.packets module
--------------------
.. automodule:: aprsd.packets
:members:
:undoc-members:
:show-inheritance:
aprsd.plugin module
-------------------
@ -116,6 +84,14 @@ aprsd.plugin\_utils module
:undoc-members:
:show-inheritance:
aprsd.rpc\_server module
------------------------
.. automodule:: aprsd.rpc_server
:members:
:undoc-members:
:show-inheritance:
aprsd.stats module
------------------
@ -124,30 +100,6 @@ aprsd.stats module
:undoc-members:
:show-inheritance:
aprsd.threads module
--------------------
.. automodule:: aprsd.threads
:members:
:undoc-members:
:show-inheritance:
aprsd.trace module
------------------
.. automodule:: aprsd.trace
:members:
:undoc-members:
:show-inheritance:
aprsd.utils module
------------------
.. automodule:: aprsd.utils
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -1,9 +1,339 @@
CHANGES
=======
* Decouple admin web interface from server command
* Dockerfile now produces aprsd.conf
* Fix some unit tests and loading of CONF w/o file
* Added missing conf
* Removed references to old custom config
* Convert config to oslo\_config
* Added rain formatting unit tests to WeatherPacket
* Fix Rain reporting in WeatherPacket send
* Removed Packet.send()
* Removed watchlist plugins
* Fix PluginManager.get\_plugins
* Cleaned up PluginManager
* Cleaned up PluginManager
* Update routing for weatherpacket
* Fix some WeatherPacket formatting
* Fix pep8 violation
* Add packet filtering for aprsd listen
* Added WeatherPacket encoding
* Updated webchat and listen for queue based RX
* reworked collecting and reporting stats
* Removed unused threading code
* Change RX packet processing to enqueu
* Make tracking objectstores work w/o initializing
* Cleaned up packet transmit class attributes
* Fix packets timestamp to int
* More messaging -> packets cleanup
* Cleaned out all references to messaging
* Added contructing a GPSPacket for sending
* cleanup webchat
* Reworked all packet processing
* Updated plugins and plugin interfaces for Packet
* Started using dataclasses to describe packets
v2.6.1
------
* v2.6.1
* Fixed position report for webchat beacon
* Try and fix broken 32bit qemu builds on 64bit system
* Add unit tests for webchat
* remove armv7 build RUST sucks
* Fix for Collections change in 3.10
v2.6.0
------
* Update workflow again
* Update Dockerfile to 22.04
* Update Dockerfile and build.sh
* Update workflow
* Prep for 2.6.0 release
* Update requirements
* Removed Makefile comment
* Update Makefile for dev vs. run environments
* Added pyopenssl for https for webchat
* change from device-detector to user-agents
* Remove twine from dev-requirements
* Update to latest Makefile.venv
* Refactored threads a bit
* Mark packets as acked in MsgTracker
* remove dev setting for template
* Add GPS beacon to mobile page
* Allow werkzeug for admin interface
* Allow werkzeug for admin interface
* Add support for mobile browsers for webchat
* Ignore callsign case while processing packets
* remove linux/arm/v7 for official builds for now
* added workflow for building specific version
* Allow passing in version to the Dockerfile
* Send GPS Beacon from webchat interface
* specify Dockerfile-dev
* Fixed build.sh
* Build on the source not released aprsd
* Remove email validation
* Add support for building linux/arm/v7
* Remove python 3.7 from docker build github
* Fixed failing unit tests
* change github workflow
* Removed TimeOpenCageDataPlugin
* Dump config with aprsd dev test-plugin
* Updated requirements
* Got webchat working with KISS tcp
* Added click auto\_envvar\_prefix
* Update aprsd thread base class to use queue
* Update packets to use wrapt
* Add remving existing requirements
* Try sending raw APRSFrames to aioax25
* Use new aprsd.callsign as the main callsign
* Fixed access to threads refactor
* Added webchat command
* Moved log.py to logging
* Moved trace.py to utils
* Fixed pep8 errors
* Refactored threads.py
* Refactor utils to directory
* remove arm build for now
* Added rustc and cargo to Dockerfile
* remove linux/arm/v6 from docker platform build
* Only tag master build as master
* Remove docker build from test
* create master-build.yml
* Added container build action
* Update docs on using Docker
* Update dev-requirements pip-tools
* Fix typo in docker-compose.yml
* Fix PyPI scraping
* Allow web interface when running in Docker
* Fix typo on exception
* README formatting fixes
* Bump dependencies to fix python 3.10
* Fixed up config option checking for KISS
* Fix logging issue with log messages
* for 2.5.9
v2.5.9
------
* FIX: logging exceptions
* Updated build and run for rich lib
* update build for 2.5.8
v2.5.8
------
* For 2.5.8
* Removed debug code
* Updated list-plugins
* Renamed virtualenv dir to .aprsd-venv
* Added unit tests for dev test-plugin
* Send Message command defaults to config
v2.5.7
------
* Updated Changelog
* Fixed an KISS config disabled issue
* Fixed a bug with multiple notify plugins enabled
* Unify the logging to file and stdout
* Added new feature to list-plugins command
* more README.rst cleanup
* Updated README examples
v2.5.6
------
* Changelog
* Tightened up the packet logging
* Added unit tests for USWeatherPlugin, USMetarPlugin
* Added test\_location to test LocationPlugin
* Updated pytest output
* Added py39 to tox for tests
* Added NotifyPlugin unit tests and more
* Small cleanup on packet logging
* Reduced the APRSIS connection reset to 2 minutes
* Fixed the NotifyPlugin
* Fixed some pep8 errors
* Add tracing for dev command
* Added python rich library based logging
* Added LOG\_LEVEL env variable for the docker
v2.5.5
------
* Update requirements to use aprslib 0.7.0
* fixed the failure during loading for objectstore
* updated docker build
v2.5.4
------
* Updated Changelog
* Fixed dev command missing initialization
v2.5.3
------
* Fix admin logging tab
v2.5.2
------
* Added new list-plugins command
* Don't require check-version command to have a config
* Healthcheck command doesn't need the aprsd.yml config
* Fix test failures
* Removed requirement for aprs.fi key
* Updated Changelog
v2.5.1
------
* Removed stock plugin
* Removed the stock plugin
v2.5.0
------
* Updated for v2.5.0
* Updated Dockerfile's and build script for docker
* Cleaned up some verbose output & colorized output
* Reworked all the common arguments
* Fixed test-plugin
* Ensure common params are honored
* pep8
* Added healthcheck to the cmds
* Removed the need for FROMCALL in dev test-plugin
* Pep8 failures
* Refactor the cli
* Updated Changelog for 4.2.3
* Fixed a problem with send-message command
v2.4.2
------
* Updated Changelog
* Be more careful picking data to/from disk
* Updated Changelog
v2.4.1
------
* Ensure plugins are last to be loaded
* Fixed email connecting to smtp server
v2.4.0
------
* Updated Changelog for 2.4.0 release
* Converted MsgTrack to ObjectStoreMixin
* Fixed unit tests
* Make sure SeenList update has a from in packet
* Ensure PacketList is initialized
* Added SIGTERM to signal\_handler
* Enable configuring where to save the objectstore data
* PEP8 cleanup
* Added objectstore Mixin
* Added -num option to aprsd-dev test-plugin
* Only call stop\_threads if it exists
* Added new SeenList
* Added plugin version to stats reporting
* Added new HelpPlugin
* Updated aprsd-dev to use config for logfile format
* Updated build.sh
* removed usage of config.check\_config\_option
* Fixed send-message after config/client rework
* Fixed issue with flask config
* Added some server startup info logs
* Increase email delay to +10
* Updated dev to use plugin manager
* Fixed notify plugins
* Added new Config object
* Fixed email plugin's use of globals
* Refactored client classes
* Refactor utils usage
* 2.3.1 Changelog
v2.3.1
------
* Fixed issue of aprs-is missing keepalive
* Fixed packet processing issue with aprsd send-message
v2.3.0
------
* Prep 2.3.0
* Enable plugins to return message object
* Added enabled flag for every plugin object
* Ensure plugin threads are valid
* Updated Dockerfile to use v2.3.0
* Removed fixed size on logging queue
* Added Logfile tab in Admin ui
* Updated Makefile clean target
* Added self creating Makefile help target
* Update dev.py
* Allow passing in aprsis\_client
* Fixed a problem with the AVWX plugin not working
* Remove some noisy trace in email plugin
* Fixed issue at startup with notify plugin
* Fixed email validation
* Removed values from forms
* Added send-message to the main admin UI
* Updated requirements
* Cleaned up some pep8 failures
* Upgraded the send-message POC to use websockets
* New Admin ui send message page working
* Send Message via admin Web interface
* Updated Admin UI to show KISS connections
* Got TX/RX working with aioax25+direwolf over TCP
* Rebased from master
* Added the ability to use direwolf KISS socket
* Update Dockerfile to use 2.2.1
v2.2.1
------
* Update Changelog for 2.2.1
* Silence some log noise
v2.2.0
------
* Updated Changelog for v2.2.0
* Updated overview image
* Removed Black code style reference
* Removed TXThread
* Added days to uptime string formatting
* Updated select timeouts
* Rebase from master and run gray
* Added tracking plugin processing
* Added threads functions to APRSDPluginBase
* Refactor Message processing and MORE
* Use Gray instead of Black for code formatting
* Updated tox.ini
* Fixed LOG.debug issue in weather plugin
* Updated slack channel link
* Cleanup of the README.rst
* Fixed aprsd-dev
v2.1.0
------
* Prep for v2.1.0
* Enable multiple replies for plugins
* Put in a fix for aprslib parse exceptions
* Fixed time plugin
* Updated the charts Added the packets chart
* Added showing symbol images to watch list
v2.0.0
------
* Updated docs for 2.0.0
* Reworked the notification threads and admin ui
* Fixed small bug with packets get\_packet\_type
* Updated overview images

View File

@ -64,7 +64,7 @@ master_doc = "index"
#
# 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.

View File

@ -15,10 +15,11 @@ a sample config file in the default location of ~/.config/aprsd/aprsd.yml
.. code-block:: shell
└─[$] -> aprsd server
Load config
/home/aprsd/.config/aprsd/aprsd.yml is missing, creating config file
Default config file created at /home/aprsd/.config/aprsd/aprsd.yml. Please edit with your settings.
└─> aprsd server
12/28/2022 04:26:31 PM MainThread ERROR No config file found!! run 'aprsd sample-config' cli_helper.py:90
12/28/2022 04:26:31 PM MainThread ERROR Config aprs_network.password not set. client.py:105
12/28/2022 04:26:31 PM MainThread ERROR Option 'aprs_network.password is not set.' was not in config file client.py:268
12/28/2022 04:26:31 PM MainThread ERROR APRS client is not properly configured in config file. server.py:58
You can see the sample config file output
@ -27,43 +28,310 @@ Sample config file
.. code-block:: shell
└─[$] -> cat ~/.config/aprsd/aprsd.yml
aprs:
host: rotate.aprs.net
logfile: /tmp/arsd.log
login: someusername
password: somepassword
port: 14580
aprsd:
enabled_plugins:
- aprsd.plugins.email.EmailPlugin
- aprsd.plugins.fortune.FortunePlugin
- aprsd.plugins.location.LocationPlugin
- aprsd.plugins.ping.PingPlugin
- aprsd.plugins.query.QueryPlugin
- aprsd.plugins.time.TimePlugin
- aprsd.plugins.weather.WeatherPlugin
- aprsd.plugins.version.VersionPlugin
plugin_dir: ~/.config/aprsd/plugins
ham:
callsign: KFART
imap:
host: imap.gmail.com
login: imapuser
password: something here too
port: 993
use_ssl: true
shortcuts:
aa: 5551239999@vtext.com
cl: craiglamparter@somedomain.org
wb: 555309@vtext.com
smtp:
host: imap.gmail.com
login: something
password: some lame password
port: 465
use_ssl: false
└─> aprsd sample-config
[DEFAULT]
#
# From aprsd.conf
#
# Callsign to use for messages sent by APRSD (string value)
#callsign = <None>
# Enable saving of watch list, packet tracker between restarts.
# (boolean value)
#enable_save = true
# Save location for packet tracking files. (string value)
#save_location = ~/.config/aprsd
# Enable code tracing (boolean value)
#trace_enabled = false
# Units for display, imperial or metric (string value)
#units = imperial
# Comma separated list of enabled plugins for APRSD.To enable
# installed external plugins add them here.The full python path to the
# class name must be used (list value)
#enabled_plugins = aprsd.plugins.email.EmailPlugin,aprsd.plugins.fortune.FortunePlugin,aprsd.plugins.location.LocationPlugin,aprsd.plugins.ping.PingPlugin,aprsd.plugins.query.QueryPlugin,aprsd.plugins.time.TimePlugin,aprsd.plugins.weather.OWMWeatherPlugin,aprsd.plugins.version.VersionPlugin
[admin]
#
# From aprsd.conf
#
# Enable the Admin Web Interface (boolean value)
#web_enabled = false
# The ip address to listen on (IP address value)
#web_ip = 0.0.0.0
# The port to listen on (port value)
# Minimum value: 0
# Maximum value: 65535
#web_port = 8001
# The admin user for the admin web interface (string value)
#user = admin
# Admin interface password (string value)
#password = <None>
[aprs_fi]
#
# From aprsd.conf
#
# Get the apiKey from your aprs.fi account here:http://aprs.fi/account
# (string value)
#apiKey = <None>
[aprs_network]
#
# From aprsd.conf
#
# Set enabled to False if there is no internet connectivity.This is
# useful for a direwolf KISS aprs connection only. (boolean value)
#enabled = true
# APRS Username (string value)
#login = NOCALL
# APRS Password Get the passcode for your callsign here:
# https://apps.magicbug.co.uk/passcode (string value)
#password = <None>
# The APRS-IS hostname (hostname value)
#host = noam.aprs2.net
# APRS-IS port (port value)
# Minimum value: 0
# Maximum value: 65535
#port = 14580
[aprsd_weewx_plugin]
#
# From aprsd_weewx_plugin.conf
#
# Latitude of the station you want to report as (floating point value)
#latitude = <None>
# Longitude of the station you want to report as (floating point
# value)
#longitude = <None>
# How long (in seconds) in between weather reports (integer value)
#report_interval = 60
[avwx_plugin]
#
# From aprsd.conf
#
# avwx-api is an opensource project that hasa hosted service here:
# https://avwx.rest/You can launch your own avwx-api in a containerby
# cloning the githug repo here:https://github.com/avwx-rest/AVWX-API
# (string value)
#apiKey = <None>
# The base url for the avwx API. If you are hosting your ownHere is
# where you change the url to point to yours. (string value)
#base_url = https://avwx.rest
[email_plugin]
#
# From aprsd.conf
#
# (Required) Callsign to validate for doing email commands.Only this
# callsign can check email. This is also where the email notifications
# for new emails will be sent. (string value)
#callsign = <None>
# Enable the Email plugin? (boolean value)
#enabled = false
# Enable the Email plugin Debugging? (boolean value)
#debug = false
# Login username/email for IMAP server (string value)
#imap_login = <None>
# Login password for IMAP server (string value)
#imap_password = <None>
# Hostname/IP of the IMAP server (hostname value)
#imap_host = <None>
# Port to use for IMAP server (port value)
# Minimum value: 0
# Maximum value: 65535
#imap_port = 993
# Use SSL for connection to IMAP Server (boolean value)
#imap_use_ssl = true
# Login username/email for SMTP server (string value)
#smtp_login = <None>
# Login password for SMTP server (string value)
#smtp_password = <None>
# Hostname/IP of the SMTP server (hostname value)
#smtp_host = <None>
# Port to use for SMTP server (port value)
# Minimum value: 0
# Maximum value: 65535
#smtp_port = 465
# Use SSL for connection to SMTP Server (boolean value)
#smtp_use_ssl = true
# List of email shortcuts for checking/sending email For Exmaple:
# wb=walt@walt.com,cl=cl@cl.com
# Means use 'wb' to send an email to walt@walt.com (list value)
#email_shortcuts = <None>
[kiss_serial]
#
# From aprsd.conf
#
# Enable Serial KISS interface connection. (boolean value)
#enabled = false
# Serial Device file to use. /dev/ttyS0 (string value)
#device = <None>
# The Serial device baud rate for communication (integer value)
#baudrate = 9600
[kiss_tcp]
#
# From aprsd.conf
#
# Enable Serial KISS interface connection. (boolean value)
#enabled = false
# The KISS TCP Host to connect to. (hostname value)
#host = <None>
# The KISS TCP/IP network port (port value)
# Minimum value: 0
# Maximum value: 65535
#port = 8001
[logging]
#
# From aprsd.conf
#
# Date format for log entries (string value)
#date_format = %m/%d/%Y %I:%M:%S %p
# Enable Rich logging (boolean value)
#rich_logging = true
# File to log to (string value)
#logfile = <None>
# Log file format, unless rich_logging enabled. (string value)
#logformat = [%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s] %(message)s - [%(pathname)s:%(lineno)d]
[owm_weather_plugin]
#
# From aprsd.conf
#
# OWMWeatherPlugin api key to OpenWeatherMap's API.This plugin uses
# the openweathermap API to fetchlocation and weather information.To
# use this plugin you need to get an openweathermapaccount and
# apikey.https://home.openweathermap.org/api_keys (string value)
#apiKey = <None>
[query_plugin]
#
# From aprsd.conf
#
# The Ham callsign to allow access to the query plugin from RF.
# (string value)
#callsign = <None>
[rpc_settings]
#
# From aprsd.conf
#
# Enable RPC calls (boolean value)
#enabled = true
# The ip address to listen on (string value)
#ip = localhost
# The port to listen on (port value)
# Minimum value: 0
# Maximum value: 65535
#port = 18861
# Magic word to authenticate requests between client/server (string
# value)
#magic_word = CHANGEME!!!
[watch_list]
#
# From aprsd.conf
#
# Enable the watch list feature. Still have to enable the correct
# plugin. Built-in plugin to use is aprsd.plugins.notify.NotifyPlugin
# (boolean value)
#enabled = false
# Callsigns to watch for messsages (list value)
#callsigns = <None>
# The Ham Callsign to send messages to for watch list alerts. (string
# value)
#alert_callsign = <None>
# The number of packets to store. (integer value)
#packet_keep_count = 10
# Time to wait before alert is sent on new message for users in
# callsigns. (integer value)
#alert_time_seconds = 3600
Note, You must edit the config file and change the ham callsign to your
legal FCC HAM callsign, or aprsd server will not start.

View File

@ -41,7 +41,7 @@ aprsd/examples/plugins/example_plugin.py
LOG = logging.getLogger("APRSD")
class HelloPlugin(plugin.APRSDPluginBase):
class HelloPlugin(plugin.APRSDRegexCommandPluginBase):
"""Hello World."""
version = "1.0"
@ -49,7 +49,7 @@ aprsd/examples/plugins/example_plugin.py
command_regex = "^[hH]"
command_name = "hello"
def command(self, fromcall, message, ack):
def process(self, packet):
LOG.info("HelloPlugin")
reply = "Hello '{}'".format(fromcall)
reply = "Hello '{}'".format(packet.from_call)
return reply

View File

@ -1,28 +1,12 @@
=====
APRSD
=====
by KM6LYW and WB4BOR
===============================================
APRSD - Ham radio APRS-IS Message plugin server
===============================================
.. image:: https://badge.fury.io/py/aprsd.svg
:target: https://badge.fury.io/py/aprsd
KM6LYW and WB4BOR
____________________
.. image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg
:target: https://github.com/craigerl/aprsd/actions
|pypi| |pytest| |versions| |slack| |issues| |commit| |imports| |down|
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://black.readthedocs.io/en/stable/
.. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336
:target: https://timothycrosley.github.io/isort/
.. image:: https://img.shields.io/github/issues/craigerl/aprsd
.. image:: https://img.shields.io/github/last-commit/craigerl/aprsd
.. image:: https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads
:target: https://pepy.tech/project/aprsd
.. contents:: :local:
`APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python.
@ -37,11 +21,14 @@ provide responding to messages to check email, get location, ping,
time of day, get weather, and fortune telling as well as version information
of aprsd itself.
Documentation: https://aprsd.readthedocs.io
Please `read the docs`_ to learn more!
.. contents:: :local:
APRSD Overview Diagram
----------------------
======================
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
@ -50,7 +37,7 @@ Typical use case
================
Ham radio operator using an APRS enabled HAM radio sends a message to check
the weather. an APRS message is sent, and then picked up by APRSD. The
the weather. An APRS message is sent, and then picked up by APRSD. The
APRS packet is decoded, and the message is sent through the list of plugins
for processing. For example, the WeatherPlugin picks up the message, fetches the weather
for the area around the user who sent the request, and then responds with
@ -59,103 +46,91 @@ callsigns to look out for. The watch list can notify you when a HAM callsign
in the list is seen and now available to message on the APRS network.
APRSD Capabilities
==================
* server - The main aprsd server processor. Send/Rx APRS messages to HAM callsign
* send-message - use aprsd to send a command/message to aprsd server. Used for development testing
* sample-config - generate a sample aprsd.yml config file for use/editing
* bash completion generation. Uses python click bash completion to generate completion code for your .bashrc/.zshrc
List of core server plugins
===========================
Plugins function by specifying a regex that is searched for in the APRS message.
If it matches, the plugin runs. IF the regex doesn't match, the plugin is skipped.
* EmailPlugin - Check email and reply with contents. Have to configure IMAP and SMTP settings in aprs.yml
* FortunePlugin - Replies with old unix fortune random fortune!
* LocationPlugin - Checks location of ham operator
* PingPlugin - Sends pong with timestamp
* QueryPlugin - Allows querying the list of delayed messages that were not ACK'd by radio
* TimePlugin - Current time of day
* WeatherPlugin - Get weather conditions for current location of HAM callsign
* VersionPlugin - Reports the version information for aprsd
List of core notification plugins
=================================
These plugins see all APRS messages from ham callsigns in the config's watch
list.
* NotifySeenPlugin - Send a message when a message is seen from a callsign in
the watch list. This is helpful when you want to know
when a friend is online in the ARPS network, but haven't
been seen in a while.
Current messages this will respond to:
Current list of built-in plugins
======================================
::
APRS messages:
l(ocation) [callsign] = descriptive current location of your radio
8 Miles E Auburn CA 1673' 39.92150,-120.93950 0.1h ago
w(eather) = weather forecast for your radio's current position
58F(58F/46F) Partly Cloudy. Tonight, Heavy Rain.
t(ime) = respond with the current time
f(ortune) = respond with a short fortune
-email_addr email text = send an email, say "mapme" to send a current position/map
-2 = resend the last 2 emails from your imap inbox to this radio
p(ing) = respond with Pong!/time
v(ersion) = Respond with current APRSD Version string
anything else = respond with usage
└─> aprsd list-plugins
🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
Meanwhile this code will monitor a single imap mailbox and forward email
to your BASECALLSIGN over the air. Only radios using the BASECALLSIGN are allowed
to send email, so consider this security risk before using this (or Amatuer radio in
general). Email is single user at this time.
Pypi.org APRSD Installable Plugin Packages
There are additional parameters in the code (sorry), so be sure to set your
email server, and associated logins, passwords. search for "yourdomain",
"password". Search for "shortcuts" to setup email aliases as well.
Install any of the following plugins with 'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
Installation:
🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
Installation
=============
pip install aprsd
To install ``aprsd``, use Pip:
Example usage:
``pip install aprsd``
Example usage
==============
aprsd -h
``aprsd -h``
Help
====
::
└─[$] > aprsd -h
└─> aprsd -h
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
Shell completion for click-completion-command Available shell types:
bash Bourne again shell fish Friendly interactive shell
powershell Windows PowerShell zsh Z shell Default type: auto
Options:
--version Show the version and exit.
-h, --help Show this message and exit.
Commands:
install Install the click-completion-command completion
sample-config This dumps the config to stdout.
check-version Check this version against the latest in pypi.org.
completion Click Completion subcommands
dev Development type subcommands
healthcheck Check the health of the running aprsd server.
list-plugins List the built in plugins available to APRSD.
listen Listen to packets on the APRS-IS Network based on FILTER.
sample-config Generate a sample Config file from aprsd and all...
send-message Send a message to a callsign via APRS_IS.
server Start the aprsd server process.
show Show the click-completion-command completion code
server Start the aprsd server gateway process.
version Show the APRSD version.
webchat Web based HAM Radio chat program!
@ -165,90 +140,14 @@ Commands
Configuration
=============
This command outputs a sample config yml formatted block that you can edit
and use to pass in to aprsd with -c. By default aprsd looks in ~/.config/aprsd/aprsd.yml
and use to pass in to ``aprsd`` with ``-c``. By default aprsd looks in ``~/.config/aprsd/aprsd.yml``
aprsd sample-config
``aprsd sample-config``
Output
======
::
└─> aprsd sample-config
aprs:
# Get the passcode for your callsign here:
# https://apps.magicbug.co.uk/passcode
host: rotate.aprs2.net
login: CALLSIGN
password: '00000'
port: 14580
aprsd:
dateformat: '%m/%d/%Y %I:%M:%S %p'
email:
enabled: true
imap:
debug: false
host: imap.gmail.com
login: IMAP_USERNAME
password: IMAP_PASSWORD
port: 993
use_ssl: true
shortcuts:
aa: 5551239999@vtext.com
cl: craiglamparter@somedomain.org
wb: 555309@vtext.com
smtp:
debug: false
host: smtp.gmail.com
login: SMTP_USERNAME
password: SMTP_PASSWORD
port: 465
use_ssl: false
enabled_plugins:
- aprsd.plugins.email.EmailPlugin
- aprsd.plugins.fortune.FortunePlugin
- aprsd.plugins.location.LocationPlugin
- aprsd.plugins.ping.PingPlugin
- aprsd.plugins.query.QueryPlugin
- aprsd.plugins.stock.StockPlugin
- aprsd.plugins.time.TimePlugin
- aprsd.plugins.weather.USWeatherPlugin
- aprsd.plugins.version.VersionPlugin
logfile: /tmp/aprsd.log
logformat: '[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s] %(message)s - [%(pathname)s:%(lineno)d]'
trace: false
units: imperial
web:
enabled: true
host: 0.0.0.0
logging_enabled: true
port: 8001
users:
admin: aprsd
ham:
callsign: CALLSIGN
services:
aprs.fi:
# Get the apiKey from your aprs.fi account here:
# http://aprs.fi/account
apiKey: APIKEYVALUE
avwx:
# (Optional for AVWXWeatherPlugin)
# Use hosted avwx-api here: https://avwx.rest
# or deploy your own from here:
# https://github.com/avwx-rest/avwx-api
apiKey: APIKEYVALUE
base_url: http://host:port
opencagedata:
# (Optional for TimeOpenCageDataPlugin)
# Get the apiKey from your opencagedata account here:
# https://opencagedata.com/dashboard#api-keys
apiKey: APIKEYVALUE
openweathermap:
# (Optional for OWMWeatherPlugin)
# Get the apiKey from your
# openweathermap account here:
# https://home.openweathermap.org/api_keys
apiKey: APIKEYVALUE
...
server
======
@ -259,35 +158,35 @@ look for incomming commands to the callsign configured in the config file
::
└─[$] > aprsd server --help
Usage: aprsd server [OPTIONS]
Usage: aprsd server [OPTIONS]
Start the aprsd server process.
Start the aprsd server gateway process.
Options:
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
The log level to use for aprsd.log
[default: INFO]
Options:
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
The log level to use for aprsd.log
[default: INFO]
-c, --config TEXT The aprsd config file to use for options.
[default:
/Users/i530566/.config/aprsd/aprsd.yml]
--quiet Don't log to stdout
-f, --flush Flush out all old aged messages on disk.
[default: False]
-h, --help Show this message and exit.
--quiet Don't log to stdout
--disable-validation Disable email shortcut validation. Bad
email addresses can result in broken email
responses!!
-c, --config TEXT The aprsd config file to use for options.
[default:
/home/waboring/.config/aprsd/aprsd.yml]
-f, --flush Flush out all old aged messages on disk.
[default: False]
-h, --help Show this message and exit.
$ aprsd server
└─> aprsd server
Load config
[02/13/2021 09:22:09 AM] [MainThread ] [INFO ] APRSD Started version: 1.6.0
[02/13/2021 09:22:09 AM] [MainThread ] [INFO ] Checking IMAP configuration
[02/13/2021 09:22:09 AM] [MainThread ] [INFO ] Checking SMTP configuration
[02/13/2021 09:22:10 AM] [MainThread ] [INFO ] Validating 2 Email shortcuts. This can take up to 10 seconds per shortcut
12/07/2021 03:16:17 PM MainThread INFO APRSD is up to date server.py:51
12/07/2021 03:16:17 PM MainThread INFO APRSD Started version: 2.5.6 server.py:52
12/07/2021 03:16:17 PM MainThread INFO Using CONFIG values: server.py:55
12/07/2021 03:16:17 PM MainThread INFO ham.callsign = WB4BOR server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.login = WB4BOR-12 server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.password = XXXXXXXXXXXXXXXXXXX server.py:58
12/07/2021 03:16:17 PM MainThread INFO aprs.host = noam.aprs2.net server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.port = 14580 server.py:60
12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60
send-message
@ -299,32 +198,30 @@ test messages
::
└─[$] > aprsd send-message -h
Usage: aprsd send-message [OPTIONS] TOCALLSIGN [COMMAND]...
Usage: aprsd send-message [OPTIONS] TOCALLSIGN COMMAND...
Send a message to a callsign via APRS_IS.
Options:
--loglevel [CRITICAL|ERROR|WARNING|INFO|DEBUG]
The log level to use for aprsd.log
[default: DEBUG]
--quiet Don't log to stdout
[default: INFO]
-c, --config TEXT The aprsd config file to use for options.
[default: ~/.config/aprsd/aprsd.yml]
[default:
/Users/i530566/.config/aprsd/aprsd.yml]
--quiet Don't log to stdout
--aprs-login TEXT What callsign to send the message from.
[env var: APRS_LOGIN]
--aprs-password TEXT the APRS-IS password for APRS_LOGIN [env
var: APRS_PASSWORD]
-n, --no-ack Don't wait for an ack, just sent it to APRS-
IS and bail. [default: False]
-w, --wait-response Wait for a response to the message?
[default: False]
--raw TEXT Send a raw message. Implies --no-ack
-h, --help Show this message and exit.
Example output:
===============
SEND EMAIL (radio to smtp server)
=================================
@ -395,25 +292,35 @@ AND... ping, fortune, time.....
Development
===========
* git clone git@github.com:craigerl/aprsd.git
* cd aprsd
* make
* ``git clone git@github.com:craigerl/aprsd.git``
* ``cd aprsd``
* ``make``
Workflow
========
While working aprsd, The workflow is as follows
While working aprsd, The workflow is as follows:
* Checkout a new branch to work on by running
``git checkout -b mybranch``
* Make your changes to the code
* Run Tox with the following options:
- ``tox -epep8``
- ``tox -efmt``
- ``tox -p``
* Commit your changes. This will run the pre-commit hooks which does checks too
``git commit``
* checkout a new branch to work on
* git checkout -b mybranch
* Edit code
* run tox -epep8
* run tox -efmt
* run tox -p
* git commit ( This will run the pre-commit hooks which does checks too )
* Once you are done with all of your commits, then push up the branch to
github
* git push -u origin mybranch
github with:
``git push -u origin mybranch``
* Create a pull request from your branch so github tests can run and we can do
a code review.
@ -423,21 +330,21 @@ Release
To do release to pypi:
* Tag release with
* Tag release with:
git tag -v1.XX -m "New release"
``git tag -v1.XX -m "New release"``
* push release tag up
* Push release tag:
git push origin master --tags
``git push origin master --tags``
* Do a test build and verify build is valid
* Do a test build and verify build is valid by running:
make build
``make build``
* Once twine is happy, upload release to pypi
* Once twine is happy, upload release to pypi:
make upload
``make upload``
Docker Container
@ -455,24 +362,62 @@ the repo.
Official Build
==============
docker build -t hemna6969/aprsd:latest .
``docker build -t hemna6969/aprsd:latest .``
Development Build
=================
docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .
``docker build -t hemna6969/aprsd:latest -f Dockerfile-dev .``
Running the container
=====================
There is a docker-compose.yml file that can be used to run your container.
There are 2 volumes defined that can be used to store your configuration
and the plugins directory: /config and /plugins
There is a ``docker-compose.yml`` file in the ``docker/`` directory
that can be used to run your container. To provide the container
an ``aprsd.conf`` configuration file, change your
``docker-compose.yml`` as shown below:
If you want to install plugins at container start time, then use the
environment var in docker-compose.yml specified as APRS_PLUGINS
Provide a csv list of pypi installable plugins. Then make sure the plugin
python file is in your /plugins volume and the plugin will be installed at
container startup. The plugin may have dependencies that are required.
The plugin file should be copied to /plugins for loading by aprsd
::
volumes:
- $HOME/.config/aprsd:/config
To install plugins at container start time, pass in a list of
comma-separated list of plugins on PyPI using the ``APRSD_PLUGINS``
environment variable in the ``docker-compose.yml`` file. Note that
version constraints may also be provided. For example:
::
environment:
- APRSD_PLUGINS=aprsd-slack-plugin>=1.0.2,aprsd-twitter-plugin
.. badges
.. |pypi| image:: https://badge.fury.io/py/aprsd.svg
:target: https://badge.fury.io/py/aprsd
.. |pytest| image:: https://github.com/craigerl/aprsd/workflows/python/badge.svg
:target: https://github.com/craigerl/aprsd/actions
.. |versions| image:: https://img.shields.io/pypi/pyversions/aprsd.svg
:target: https://pypi.org/pypi/aprsd
.. |slack| image:: https://img.shields.io/badge/slack-@hemna/aprsd-blue.svg?logo=slack
:target: https://hemna.slack.com/app_redirect?channel=C01KQSCP5RP
.. |imports| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336
:target: https://timothycrosley.github.io/isort/
.. |issues| image:: https://img.shields.io/github/issues/craigerl/aprsd
.. |commit| image:: https://img.shields.io/github/last-commit/craigerl/aprsd
.. |down| image:: https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads
:target: https://pepy.tech/project/aprsd
.. links
.. _read the docs:
https://aprsd.readthedocs.io

View File

@ -23,9 +23,11 @@ beautifulsoup4
wrapt
# kiss3 uses attrs
kiss3
attrs==22.1.0
attrs
# for mobile checking
user-agents
pyopenssl
dataclasses
dacite2
oslo.config
rpyc

View File

@ -1,15 +1,15 @@
#
# This file is autogenerated by pip-compile with Python 3.9
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --annotation-style=line --resolver=backtracking requirements.in
#
aprslib==0.7.2 # via -r requirements.in
attrs==22.1.0 # via -r requirements.in, ax253, kiss3
attrs==22.2.0 # via -r requirements.in, ax253, kiss3
ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.11.1 # via -r requirements.in
bidict==0.22.0 # via python-socketio
bitarray==2.6.0 # via ax253, kiss3
bitarray==2.6.1 # via ax253, kiss3
certifi==2022.12.7 # via requests
cffi==1.15.1 # via cryptography
charset-normalizer==2.1.1 # via requests
@ -19,6 +19,7 @@ commonmark==0.9.1 # via rich
cryptography==38.0.4 # via pyopenssl
dacite2==2.0.0 # via -r requirements.in
dataclasses==0.6 # via -r requirements.in
debtcollector==2.5.0 # via oslo-config
dnspython==2.2.1 # via eventlet
eventlet==0.33.2 # via -r requirements.in
flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio
@ -28,13 +29,17 @@ flask-socketio==5.3.2 # via -r requirements.in
greenlet==2.0.1 # via eventlet
idna==3.4 # via requests
imapclient==2.3.1 # via -r requirements.in
importlib-metadata==5.1.0 # via ax253, flask, kiss3
importlib-metadata==5.2.0 # via ax253, kiss3
itsdangerous==2.1.2 # via flask
jinja2==3.1.2 # via click-completion, flask
kiss3==8.0.0 # via -r requirements.in
markupsafe==2.1.1 # via jinja2
pbr==5.11.0 # via -r requirements.in
netaddr==0.8.0 # via oslo-config
oslo-config==9.0.0 # via -r requirements.in
oslo-i18n==5.1.0 # via oslo-config
pbr==5.11.0 # via -r requirements.in, oslo-i18n, stevedore
pluggy==1.0.0 # via -r requirements.in
plumbum==1.8.0 # via rpyc
pycparser==2.21 # via cffi
pygments==2.13.0 # via rich
pyopenssl==22.1.0 # via -r requirements.in
@ -42,13 +47,16 @@ pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3
python-engineio==4.3.4 # via python-socketio
python-socketio==5.7.2 # via flask-socketio
pytz==2022.6 # via -r requirements.in
pyyaml==6.0 # via -r requirements.in
requests==2.28.1 # via -r requirements.in, update-checker
pytz==2022.7 # via -r requirements.in
pyyaml==6.0 # via -r requirements.in, oslo-config
requests==2.28.1 # via -r requirements.in, oslo-config, update-checker
rfc3986==2.0.0 # via oslo-config
rich==12.6.0 # via -r requirements.in
rpyc==5.3.0 # via -r requirements.in
shellingham==1.5.0 # via click-completion
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
soupsieve==2.3.2.post1 # via beautifulsoup4
stevedore==4.1.1 # via oslo-config
tabulate==0.9.0 # via -r requirements.in
thesmuggler==1.0.1 # via -r requirements.in
ua-parser==0.16.1 # via user-agents
@ -56,5 +64,5 @@ update-checker==0.18.0 # via -r requirements.in
urllib3==1.26.13 # via requests
user-agents==2.2.0 # via -r requirements.in
werkzeug==2.1.2 # via -r requirements.in, flask
wrapt==1.14.1 # via -r requirements.in
wrapt==1.14.1 # via -r requirements.in, debtcollector
zipp==3.11.0 # via importlib-metadata

View File

@ -34,6 +34,10 @@ packages =
[entry_points]
console_scripts =
aprsd = aprsd.aprsd:main
oslo.config.opts =
aprsd.conf = aprsd.conf.opts:list_opts
oslo.config.opts.defaults =
aprsd.conf = aprsd.conf:set_lib_defaults
[build_sphinx]
source-dir = docs

View File

@ -1,68 +0,0 @@
import typing as t
import unittest
from unittest import mock
from click.testing import CliRunner
from aprsd import config as aprsd_config
from aprsd.aprsd import cli
from aprsd.cmds import dev # noqa
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestDevTestPluginCommand(unittest.TestCase):
def _build_config(self, login=None, password=None):
config = {
"aprs": {},
"aprsd": {
"trace": False,
"watch_list": {},
},
}
if login:
config["aprs"]["login"] = login
if password:
config["aprs"]["password"] = password
return aprsd_config.Config(config)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
runner = CliRunner()
mock_parse_config.return_value = self._build_config()
result = runner.invoke(
cli, [
"dev", "test-plugin",
"-p", "aprsd.plugins.version.VersionPlugin",
"bogus command",
],
catch_exceptions=False,
)
# rich.print(f"EXIT CODE {result.exit_code}")
# rich.print(f"Exception {result.exception}")
# rich.print(f"OUTPUT {result.output}")
assert result.exit_code == -1
assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_plugin_arg(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
runner = CliRunner()
mock_parse_config.return_value = self._build_config(login="something")
result = runner.invoke(
cli, ["dev", "test-plugin", "bogus command"],
catch_exceptions=False,
)
assert result.exit_code == 2
assert "Failed to provide -p option to test a plugin" in result.output

View File

@ -3,78 +3,42 @@ import unittest
from unittest import mock
from click.testing import CliRunner
from oslo_config import cfg
from aprsd import config as aprsd_config
from aprsd import conf # noqa : F401
from aprsd.aprsd import cli
from aprsd.cmds import send_message # noqa
from .. import fake
CONF = cfg.CONF
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestSendMessageCommand(unittest.TestCase):
def _build_config(self, login=None, password=None):
config = {
"aprs": {},
"aprsd": {
"trace": False,
"watch_list": {},
},
}
def config_and_init(self, login=None, password=None):
CONF.callsign = fake.FAKE_TO_CALLSIGN
CONF.trace_enabled = False
CONF.watch_list.packet_keep_count = 1
if login:
config["aprs"]["login"] = login
CONF.aprs_network.login = login
if password:
config["aprs"]["password"] = password
CONF.aprs_network.password = password
return aprsd_config.Config(config)
CONF.admin.user = "admin"
CONF.admin.password = "password"
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_login(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no login and config."""
return
runner = CliRunner()
mock_parse_config.return_value = self._build_config()
result = runner.invoke(
cli, ["send-message", "WB4BOR", "wx"],
catch_exceptions=False,
)
# rich.print(f"EXIT CODE {result.exit_code}")
# rich.print(f"Exception {result.exception}")
# rich.print(f"OUTPUT {result.output}")
assert result.exit_code == -1
assert "Must set --aprs_login or APRS_LOGIN" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_password(self, mock_logging, mock_parse_config):
"""Make sure we get an error if there is no password and config."""
return
runner = CliRunner()
mock_parse_config.return_value = self._build_config(login="something")
result = runner.invoke(
cli, ["send-message", "WB4BOR", "wx"],
catch_exceptions=False,
)
assert result.exit_code == -1
assert "Must set --aprs-password or APRS_PASSWORD" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_tocallsign(self, mock_logging, mock_parse_config):
def test_no_tocallsign(self, mock_logging):
"""Make sure we get an error if there is no tocallsign."""
runner = CliRunner()
mock_parse_config.return_value = self._build_config(
self.config_and_init(
login="something",
password="another",
)
runner = CliRunner()
result = runner.invoke(
cli, ["send-message"],
@ -83,16 +47,15 @@ class TestSendMessageCommand(unittest.TestCase):
assert result.exit_code == 2
assert "Error: Missing argument 'TOCALLSIGN'" in result.output
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_no_command(self, mock_logging, mock_parse_config):
def test_no_command(self, mock_logging):
"""Make sure we get an error if there is no command."""
runner = CliRunner()
mock_parse_config.return_value = self._build_config(
self.config_and_init(
login="something",
password="another",
)
runner = CliRunner()
result = runner.invoke(
cli, ["send-message", "WB4BOR"],

View File

@ -5,112 +5,81 @@ from unittest import mock
from click.testing import CliRunner
import flask
import flask_socketio
from oslo_config import cfg
from aprsd import config as aprsd_config
from aprsd import packets
from aprsd import conf # noqa: F401
from aprsd.cmds import webchat # noqa
from aprsd.packets import core
from .. import fake
CONF = cfg.CONF
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
class TestSendMessageCommand(unittest.TestCase):
def _build_config(self, login=None, password=None):
config = {
"aprs": {},
"aprsd": {
"trace": False,
"web": {
"users": {"admin": "password"},
},
"watch_list": {"packet_keep_count": 1},
},
}
def config_and_init(self, login=None, password=None):
CONF.callsign = fake.FAKE_TO_CALLSIGN
CONF.trace_enabled = False
CONF.watch_list.packet_keep_count = 1
if login:
config["aprs"]["login"] = login
CONF.aprs_network.login = login
if password:
config["aprs"]["password"] = password
CONF.aprs_network.password = password
return aprsd_config.Config(config)
CONF.admin.user = "admin"
CONF.admin.password = "password"
@mock.patch("aprsd.config.parse_config")
def test_missing_config(self, mock_parse_config):
CliRunner()
cfg = self._build_config()
del cfg["aprsd"]["web"]["users"]
mock_parse_config.return_value = cfg
server = webchat.WebChatFlask()
self.assertRaises(
KeyError,
server.set_config, cfg,
)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.logging.log.setup_logging")
def test_init_flask(self, mock_logging, mock_parse_config):
def test_init_flask(self, mock_logging):
"""Make sure we get an error if there is no login and config."""
CliRunner()
cfg = self._build_config()
mock_parse_config.return_value = cfg
self.config_and_init()
socketio, flask_app = webchat.init_flask(cfg, "DEBUG", False)
socketio, flask_app = webchat.init_flask("DEBUG", False)
self.assertIsInstance(socketio, flask_socketio.SocketIO)
self.assertIsInstance(flask_app, flask.Flask)
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.packets.tracker.PacketTrack.remove")
@mock.patch("aprsd.cmds.webchat.socketio.emit")
@mock.patch("aprsd.cmds.webchat.socketio")
def test_process_ack_packet(
self, mock_parse_config,
mock_remove, mock_emit,
self,
mock_remove, mock_socketio,
):
config = self._build_config()
mock_parse_config.return_value = config
self.config_and_init()
mock_socketio.emit = mock.MagicMock()
packet = fake.fake_packet(
message="blah",
msg_number=1,
message_format=core.PACKET_TYPE_ACK,
)
socketio = mock.MagicMock()
packets.PacketList(config=config)
packets.PacketTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
wcp = webchat.WebChatProcessPacketThread(config, packet, socketio)
wcp = webchat.WebChatProcessPacketThread(packet, socketio)
wcp.process_ack_packet(packet)
mock_remove.called_once()
mock_emit.called_once()
mock_socketio.called_once()
@mock.patch("aprsd.config.parse_config")
@mock.patch("aprsd.packets.PacketList.rx")
@mock.patch("aprsd.cmds.webchat.socketio.emit")
@mock.patch("aprsd.cmds.webchat.socketio")
def test_process_our_message_packet(
self, mock_parse_config,
self,
mock_packet_add,
mock_emit,
mock_socketio,
):
config = self._build_config()
mock_parse_config.return_value = config
self.config_and_init()
mock_socketio.emit = mock.MagicMock()
packet = fake.fake_packet(
message="blah",
msg_number=1,
message_format=core.PACKET_TYPE_MESSAGE,
)
socketio = mock.MagicMock()
packets.PacketList(config=config)
packets.PacketTrack(config=config)
packets.WatchList(config=config)
packets.SeenList(config=config)
wcp = webchat.WebChatProcessPacketThread(config, packet, socketio)
wcp = webchat.WebChatProcessPacketThread(packet, socketio)
wcp.process_our_message_packet(packet)
mock_packet_add.called_once()
mock_emit.called_once()
mock_socketio.called_once()

View File

@ -1,15 +1,21 @@
from unittest import mock
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import fortune as fortune_plugin
from .. import fake, test_plugin
CONF = cfg.CONF
class TestFortunePlugin(test_plugin.TestPlugin):
@mock.patch("shutil.which")
def test_fortune_fail(self, mock_which):
mock_which.return_value = None
fortune = fortune_plugin.FortunePlugin(self.config)
fortune = fortune_plugin.FortunePlugin()
expected = "FortunePlugin isn't enabled"
packet = fake.fake_packet(message="fortune")
actual = fortune.filter(packet)
@ -20,7 +26,8 @@ class TestFortunePlugin(test_plugin.TestPlugin):
def test_fortune_success(self, mock_which, mock_output):
mock_which.return_value = "/usr/bin/games/fortune"
mock_output.return_value = "Funny fortune"
fortune = fortune_plugin.FortunePlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = fortune_plugin.FortunePlugin()
expected = "Funny fortune"
packet = fake.fake_packet(message="fortune")

View File

@ -1,18 +1,24 @@
from unittest import mock
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import location as location_plugin
from .. import fake, test_plugin
CONF = cfg.CONF
class TestLocationPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.config.Config.check_option")
def test_location_not_enabled_missing_aprs_fi_key(self, mock_check):
def test_location_not_enabled_missing_aprs_fi_key(self):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.side_effect = Exception
fortune = location_plugin.LocationPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
CONF.aprs_fi.apiKey = None
fortune = location_plugin.LocationPlugin()
expected = "LocationPlugin isn't enabled"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)
@ -23,7 +29,8 @@ class TestLocationPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.side_effect = Exception
fortune = location_plugin.LocationPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin()
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)
@ -34,7 +41,8 @@ class TestLocationPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.return_value = {"entries": []}
fortune = location_plugin.LocationPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin()
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)
@ -57,7 +65,8 @@ class TestLocationPlugin(test_plugin.TestPlugin):
}
mock_weather.side_effect = Exception
mock_time.return_value = 10
fortune = location_plugin.LocationPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin()
expected = "KFAKE: Unknown Location 0' 10,11 0.0h ago"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)
@ -82,7 +91,8 @@ class TestLocationPlugin(test_plugin.TestPlugin):
wx_data = {"location": {"areaDescription": expected_town}}
mock_weather.return_value = wx_data
mock_time.return_value = 10
fortune = location_plugin.LocationPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
fortune = location_plugin.LocationPlugin()
expected = f"KFAKE: {expected_town} 0' 10,11 0.0h ago"
packet = fake.fake_packet(message="location")
actual = fortune.filter(packet)

View File

@ -1,14 +1,16 @@
from unittest import mock
from aprsd import client
from aprsd import config as aprsd_config
from aprsd import packets
from oslo_config import cfg
from aprsd import client, packets
from aprsd import conf # noqa: F401
from aprsd.plugins import notify as notify_plugin
from .. import fake, test_plugin
DEFAULT_WATCHLIST_CALLSIGNS = [fake.FAKE_FROM_CALLSIGN]
CONF = cfg.CONF
DEFAULT_WATCHLIST_CALLSIGNS = fake.FAKE_FROM_CALLSIGN
class TestWatchListPlugin(test_plugin.TestPlugin):
@ -16,7 +18,7 @@ class TestWatchListPlugin(test_plugin.TestPlugin):
self.fromcall = fake.FAKE_FROM_CALLSIGN
self.ack = 1
def _config(
def config_and_init(
self,
watchlist_enabled=True,
watchlist_alert_callsign=None,
@ -24,39 +26,33 @@ class TestWatchListPlugin(test_plugin.TestPlugin):
watchlist_packet_keep_count=None,
watchlist_callsigns=DEFAULT_WATCHLIST_CALLSIGNS,
):
_config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT)
default_wl = aprsd_config.DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]
_config["ham"]["callsign"] = self.fromcall
_config["aprsd"]["callsign"] = self.fromcall
_config["aprs"]["login"] = self.fromcall
_config["services"]["aprs.fi"]["apiKey"] = "something"
CONF.callsign = self.fromcall
CONF.aprs_network.login = self.fromcall
CONF.aprs_fi.apiKey = "something"
# Set the watchlist specific config options
CONF.watch_list.enabled = watchlist_enabled
_config["aprsd"]["watch_list"]["enabled"] = watchlist_enabled
if not watchlist_alert_callsign:
watchlist_alert_callsign = fake.FAKE_TO_CALLSIGN
_config["aprsd"]["watch_list"]["alert_callsign"] = watchlist_alert_callsign
CONF.watch_list.alert_callsign = watchlist_alert_callsign
if not watchlist_alert_time_seconds:
watchlist_alert_time_seconds = default_wl["alert_time_seconds"]
_config["aprsd"]["watch_list"]["alert_time_seconds"] = watchlist_alert_time_seconds
watchlist_alert_time_seconds = CONF.watch_list.alert_time_seconds
CONF.watch_list.alert_time_seconds = watchlist_alert_time_seconds
if not watchlist_packet_keep_count:
watchlist_packet_keep_count = default_wl["packet_keep_count"]
_config["aprsd"]["watch_list"]["packet_keep_count"] = watchlist_packet_keep_count
watchlist_packet_keep_count = CONF.watch_list.packet_keep_count
CONF.watch_list.packet_keep_count = watchlist_packet_keep_count
_config["aprsd"]["watch_list"]["callsigns"] = watchlist_callsigns
return _config
CONF.watch_list.callsigns = watchlist_callsigns
class TestAPRSDWatchListPluginBase(TestWatchListPlugin):
def test_watchlist_not_enabled(self):
config = self._config(watchlist_enabled=False)
self.config_and_init(config=config)
plugin = fake.FakeWatchListPlugin(self.config)
self.config_and_init(watchlist_enabled=False)
plugin = fake.FakeWatchListPlugin()
packet = fake.fake_packet(
message="version",
@ -69,9 +65,8 @@ class TestAPRSDWatchListPluginBase(TestWatchListPlugin):
@mock.patch("aprsd.client.ClientFactory", autospec=True)
def test_watchlist_not_in_watchlist(self, mock_factory):
client.factory = mock_factory
config = self._config()
self.config_and_init(config=config)
plugin = fake.FakeWatchListPlugin(self.config)
self.config_and_init()
plugin = fake.FakeWatchListPlugin()
packet = fake.fake_packet(
fromcall="FAKE",
@ -86,9 +81,8 @@ class TestAPRSDWatchListPluginBase(TestWatchListPlugin):
class TestNotifySeenPlugin(TestWatchListPlugin):
def test_disabled(self):
config = self._config(watchlist_enabled=False)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
self.config_and_init(watchlist_enabled=False)
plugin = notify_plugin.NotifySeenPlugin()
packet = fake.fake_packet(
message="version",
@ -101,9 +95,8 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
@mock.patch("aprsd.client.ClientFactory", autospec=True)
def test_callsign_not_in_watchlist(self, mock_factory):
client.factory = mock_factory
config = self._config(watchlist_enabled=False)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
self.config_and_init(watchlist_enabled=False)
plugin = notify_plugin.NotifySeenPlugin()
packet = fake.fake_packet(
message="version",
@ -118,12 +111,11 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
def test_callsign_in_watchlist_not_old(self, mock_is_old, mock_factory):
client.factory = mock_factory
mock_is_old.return_value = False
config = self._config(
self.config_and_init(
watchlist_enabled=True,
watchlist_callsigns=["WB4BOR"],
)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
plugin = notify_plugin.NotifySeenPlugin()
packet = fake.fake_packet(
fromcall="WB4BOR",
@ -139,13 +131,12 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
def test_callsign_in_watchlist_old_same_alert_callsign(self, mock_is_old, mock_factory):
client.factory = mock_factory
mock_is_old.return_value = True
config = self._config(
self.config_and_init(
watchlist_enabled=True,
watchlist_alert_callsign="WB4BOR",
watchlist_callsigns=["WB4BOR"],
)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
plugin = notify_plugin.NotifySeenPlugin()
packet = fake.fake_packet(
fromcall="WB4BOR",
@ -163,13 +154,12 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
mock_is_old.return_value = True
notify_callsign = fake.FAKE_TO_CALLSIGN
fromcall = "WB4BOR"
config = self._config(
self.config_and_init(
watchlist_enabled=True,
watchlist_alert_callsign=notify_callsign,
watchlist_callsigns=["WB4BOR"],
)
self.config_and_init(config=config)
plugin = notify_plugin.NotifySeenPlugin(self.config)
plugin = notify_plugin.NotifySeenPlugin()
packet = fake.fake_packet(
fromcall=fromcall,

View File

@ -1,10 +1,16 @@
from unittest import mock
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import ping as ping_plugin
from .. import fake, test_plugin
CONF = cfg.CONF
class TestPingPlugin(test_plugin.TestPlugin):
@mock.patch("time.localtime")
def test_ping(self, mock_time):
@ -14,7 +20,8 @@ class TestPingPlugin(test_plugin.TestPlugin):
s = fake_time.tm_sec = 55
mock_time.return_value = fake_time
ping = ping_plugin.PingPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
ping = ping_plugin.PingPlugin()
packet = fake.fake_packet(
message="location",

View File

@ -1,5 +1,7 @@
from unittest import mock
from oslo_config import cfg
from aprsd import packets
from aprsd.packets import tracker
from aprsd.plugins import query as query_plugin
@ -7,11 +9,18 @@ from aprsd.plugins import query as query_plugin
from .. import fake, test_plugin
CONF = cfg.CONF
class TestQueryPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.packets.tracker.PacketTrack.flush")
def test_query_flush(self, mock_flush):
packet = fake.fake_packet(message="!delete")
query = query_plugin.QueryPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
CONF.save_enabled = True
CONF.query_plugin.callsign = fake.FAKE_FROM_CALLSIGN
query = query_plugin.QueryPlugin()
query.enabled = True
expected = "Deleted ALL pending msgs."
actual = query.filter(packet)
@ -20,10 +29,13 @@ class TestQueryPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.packets.tracker.PacketTrack.restart_delayed")
def test_query_restart_delayed(self, mock_restart):
CONF.callsign = fake.FAKE_TO_CALLSIGN
CONF.save_enabled = True
CONF.query_plugin.callsign = fake.FAKE_FROM_CALLSIGN
track = tracker.PacketTrack()
track.data = {}
packet = fake.fake_packet(message="!4")
query = query_plugin.QueryPlugin(self.config)
query = query_plugin.QueryPlugin()
expected = "No pending msgs to resend"
actual = query.filter(packet)

View File

@ -1,5 +1,6 @@
from unittest import mock
from oslo_config import cfg
import pytz
from aprsd.plugins import time as time_plugin
@ -8,6 +9,9 @@ from aprsd.utils import fuzzy
from .. import fake, test_plugin
CONF = cfg.CONF
class TestTimePlugins(test_plugin.TestPlugin):
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
@ -25,7 +29,8 @@ class TestTimePlugins(test_plugin.TestPlugin):
h = int(local_t.strftime("%H"))
m = int(local_t.strftime("%M"))
fake_time.tm_sec = 13
time = time_plugin.TimePlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
time = time_plugin.TimePlugin()
packet = fake.fake_packet(
message="location",

View File

@ -1,4 +1,4 @@
from unittest import mock
from oslo_config import cfg
import aprsd
from aprsd.plugins import version as version_plugin
@ -6,11 +6,16 @@ from aprsd.plugins import version as version_plugin
from .. import fake, test_plugin
CONF = cfg.CONF
class TestVersionPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.plugin.PluginManager.get_plugins")
def test_version(self, mock_get_plugins):
def test_version(self):
expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00"
version = version_plugin.VersionPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
version = version_plugin.VersionPlugin()
version.enabled = True
packet = fake.fake_packet(
message="No",

View File

@ -1,18 +1,24 @@
from unittest import mock
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import weather as weather_plugin
from .. import fake, test_plugin
CONF = cfg.CONF
class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.config.Config.check_option")
def test_not_enabled_missing_aprs_fi_key(self, mock_check):
def test_not_enabled_missing_aprs_fi_key(self):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.side_effect = Exception
wx = weather_plugin.USWeatherPlugin(self.config)
CONF.aprs_fi.apiKey = None
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
expected = "USWeatherPlugin isn't enabled"
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
@ -23,7 +29,9 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then
# the Plugin will be disabled.
mock_check.side_effect = Exception
wx = weather_plugin.USWeatherPlugin(self.config)
CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
@ -34,7 +42,10 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then
# the Plugin will be disabled.
mock_check.return_value = {"entries": []}
wx = weather_plugin.USWeatherPlugin(self.config)
CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
wx.enabled = True
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
@ -55,7 +66,10 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
],
}
mock_weather.side_effect = Exception
wx = weather_plugin.USWeatherPlugin(self.config)
CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
wx.enabled = True
expected = "Unable to get weather"
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
@ -83,7 +97,10 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
},
"time": {"startPeriodName": ["ignored", "sometime"]},
}
wx = weather_plugin.USWeatherPlugin(self.config)
CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USWeatherPlugin()
wx.enabled = True
expected = "400F(10F/11F) test. sometime, another."
packet = fake.fake_packet(message="weather")
actual = wx.filter(packet)
@ -92,12 +109,11 @@ class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
class TestUSMetarPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.config.Config.check_option")
def test_not_enabled_missing_aprs_fi_key(self, mock_check):
def test_not_enabled_missing_aprs_fi_key(self):
# When the aprs.fi api key isn't set, then
# the LocationPlugin will be disabled.
mock_check.side_effect = Exception
wx = weather_plugin.USMetarPlugin(self.config)
CONF.aprs_fi.apiKey = None
wx = weather_plugin.USMetarPlugin()
expected = "USMetarPlugin isn't enabled"
packet = fake.fake_packet(message="metar")
actual = wx.filter(packet)
@ -108,7 +124,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then
# the Plugin will be disabled.
mock_check.side_effect = Exception
wx = weather_plugin.USMetarPlugin(self.config)
CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="metar")
actual = wx.filter(packet)
@ -119,7 +138,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
# When the aprs.fi api key isn't set, then
# the Plugin will be disabled.
mock_check.return_value = {"entries": []}
wx = weather_plugin.USMetarPlugin(self.config)
CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "Failed to fetch aprs.fi location"
packet = fake.fake_packet(message="metar")
actual = wx.filter(packet)
@ -128,7 +150,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
@mock.patch("aprsd.plugin_utils.get_weather_gov_metar")
def test_gov_metar_fetch_fails(self, mock_metar):
mock_metar.side_effect = Exception
wx = weather_plugin.USMetarPlugin(self.config)
CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "Unable to find station METAR"
packet = fake.fake_packet(message="metar KPAO")
actual = wx.filter(packet)
@ -141,7 +166,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
text = '{"properties": {"rawMessage": "BOGUSMETAR"}}'
mock_metar.return_value = Response()
wx = weather_plugin.USMetarPlugin(self.config)
CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "BOGUSMETAR"
packet = fake.fake_packet(message="metar KPAO")
actual = wx.filter(packet)
@ -169,7 +197,10 @@ class TestUSMetarPlugin(test_plugin.TestPlugin):
}
mock_metar.return_value = Response()
wx = weather_plugin.USMetarPlugin(self.config)
CONF.aprs_fi.apiKey = "abc123"
CONF.callsign = fake.FAKE_TO_CALLSIGN
wx = weather_plugin.USMetarPlugin()
wx.enabled = True
expected = "BOGUSMETAR"
packet = fake.fake_packet(message="metar")
actual = wx.filter(packet)

View File

@ -1,25 +1,32 @@
import unittest
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd.plugins import email
CONF = cfg.CONF
class TestEmail(unittest.TestCase):
def test_get_email_from_shortcut(self):
config = {"aprsd": {"email": {"shortcuts": {}}}}
CONF.email_plugin.shortcuts = None
email_address = "something@something.com"
addr = f"-{email_address}"
actual = email.get_email_from_shortcut(config, addr)
actual = email.get_email_from_shortcut(addr)
self.assertEqual(addr, actual)
config = {"aprsd": {"email": {"nothing": "nothing"}}}
actual = email.get_email_from_shortcut(config, addr)
CONF.email_plugin.shortcuts = None
actual = email.get_email_from_shortcut(addr)
self.assertEqual(addr, actual)
config = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}}
actual = email.get_email_from_shortcut(config, addr)
CONF.email_plugin.shortcuts = None
actual = email.get_email_from_shortcut(addr)
self.assertEqual(addr, actual)
config = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}}
short = "-wb"
actual = email.get_email_from_shortcut(config, short)
CONF.email_plugin.email_shortcuts = ["wb=something@something.com"]
email.shortcuts_dict = None
short = "wb"
actual = email.get_email_from_shortcut(short)
self.assertEqual(email_address, actual)

View File

@ -17,6 +17,6 @@ class TestMain(unittest.TestCase):
"""Test to make sure we fail."""
imap_mock.return_value = None
smtp_mock.return_value = {"smaiof": "fire"}
config = mock.MagicMock()
mock.MagicMock()
email.validate_email_config(config, True)
email.validate_email_config(True)

View File

@ -1,7 +1,9 @@
import unittest
from unittest import mock
from aprsd import config as aprsd_config
from oslo_config import cfg
from aprsd import conf # noqa: F401
from aprsd import packets
from aprsd import plugin as aprsd_plugin
from aprsd import plugins, stats
@ -10,6 +12,9 @@ from aprsd.packets import core
from . import fake
CONF = cfg.CONF
class TestPluginManager(unittest.TestCase):
def setUp(self) -> None:
@ -21,34 +26,26 @@ class TestPluginManager(unittest.TestCase):
aprsd_plugin.PluginManager._instance = None
def config_and_init(self):
self.config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT)
self.config["ham"]["callsign"] = self.fromcall
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
self.config["services"]["aprs.fi"]["apiKey"] = "something"
self.config["aprsd"]["enabled_plugins"] = [
"aprsd.plugins.ping.PingPlugin",
]
print(self.config)
def test_init_no_config(self):
pm = aprsd_plugin.PluginManager()
self.assertEqual(None, pm.config)
def test_init_with_config(self):
pm = aprsd_plugin.PluginManager(self.config)
self.assertEqual(self.config, pm.config)
CONF.callsign = self.fromcall
CONF.aprs_network.login = fake.FAKE_TO_CALLSIGN
CONF.aprs_fi.apiKey = "something"
CONF.enabled_plugins = "aprsd.plugins.ping.PingPlugin"
CONF.enable_save = False
def test_get_plugins_no_plugins(self):
pm = aprsd_plugin.PluginManager(self.config)
CONF.enabled_plugins = []
pm = aprsd_plugin.PluginManager()
plugin_list = pm.get_plugins()
self.assertEqual([], plugin_list)
def test_get_plugins_with_plugins(self):
pm = aprsd_plugin.PluginManager(self.config)
CONF.enabled_plugins = ["aprsd.plugins.ping.PingPlugin"]
pm = aprsd_plugin.PluginManager()
plugin_list = pm.get_plugins()
self.assertEqual([], plugin_list)
pm.setup_plugins()
plugin_list = pm.get_plugins()
print(plugin_list)
self.assertIsInstance(plugin_list, list)
self.assertIsInstance(
plugin_list[0],
@ -59,7 +56,7 @@ class TestPluginManager(unittest.TestCase):
)
def test_get_watchlist_plugins(self):
pm = aprsd_plugin.PluginManager(self.config)
pm = aprsd_plugin.PluginManager()
plugin_list = pm.get_plugins()
self.assertEqual([], plugin_list)
pm.setup_plugins()
@ -68,7 +65,8 @@ class TestPluginManager(unittest.TestCase):
self.assertEqual(0, len(plugin_list))
def test_get_message_plugins(self):
pm = aprsd_plugin.PluginManager(self.config)
CONF.enabled_plugins = ["aprsd.plugins.ping.PingPlugin"]
pm = aprsd_plugin.PluginManager()
plugin_list = pm.get_plugins()
self.assertEqual([], plugin_list)
pm.setup_plugins()
@ -98,27 +96,19 @@ class TestPlugin(unittest.TestCase):
packets.PacketTrack._instance = None
self.config = None
def config_and_init(self, config=None):
if not config:
self.config = aprsd_config.Config(aprsd_config.DEFAULT_CONFIG_DICT)
self.config["ham"]["callsign"] = self.fromcall
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
self.config["services"]["aprs.fi"]["apiKey"] = "something"
else:
self.config = config
# Inintialize the stats object with the config
stats.APRSDStats(self.config)
packets.WatchList(config=self.config)
packets.SeenList(config=self.config)
packets.PacketTrack(config=self.config)
def config_and_init(self):
CONF.callsign = self.fromcall
CONF.aprs_network.login = fake.FAKE_TO_CALLSIGN
CONF.aprs_fi.apiKey = "something"
CONF.enabled_plugins = "aprsd.plugins.ping.PingPlugin"
CONF.enable_save = False
class TestPluginBase(TestPlugin):
@mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process")
def test_base_plugin_no_threads(self, mock_process):
p = fake.FakeBaseNoThreadsPlugin(self.config)
p = fake.FakeBaseNoThreadsPlugin()
expected = []
actual = p.create_threads()
@ -139,19 +129,20 @@ class TestPluginBase(TestPlugin):
@mock.patch.object(fake.FakeBaseThreadsPlugin, "create_threads")
def test_base_plugin_threads_created(self, mock_create):
p = fake.FakeBaseThreadsPlugin(self.config)
p = fake.FakeBaseThreadsPlugin()
mock_create.assert_called_once()
p.stop_threads()
def test_base_plugin_threads(self):
p = fake.FakeBaseThreadsPlugin(self.config)
p = fake.FakeBaseThreadsPlugin()
actual = p.create_threads()
self.assertTrue(isinstance(actual, fake.FakeThread))
p.stop_threads()
@mock.patch.object(fake.FakeRegexCommandPlugin, "process")
def test_regex_base_not_called(self, mock_process):
p = fake.FakeRegexCommandPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
p = fake.FakeRegexCommandPlugin()
packet = fake.fake_packet(message="a")
expected = None
actual = p.filter(packet)
@ -165,32 +156,32 @@ class TestPluginBase(TestPlugin):
mock_process.assert_not_called()
packet = fake.fake_packet(
message="F",
message_format=core.PACKET_TYPE_MICE,
)
expected = None
expected = packets.NULL_MESSAGE
actual = p.filter(packet)
self.assertEqual(expected, actual)
mock_process.assert_not_called()
packet = fake.fake_packet(
message="f",
message_format=core.PACKET_TYPE_ACK,
)
expected = None
expected = packets.NULL_MESSAGE
actual = p.filter(packet)
self.assertEqual(expected, actual)
mock_process.assert_not_called()
@mock.patch.object(fake.FakeRegexCommandPlugin, "process")
def test_regex_base_assert_called(self, mock_process):
p = fake.FakeRegexCommandPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
p = fake.FakeRegexCommandPlugin()
packet = fake.fake_packet(message="f")
p.filter(packet)
mock_process.assert_called_once()
def test_regex_base_process_called(self):
p = fake.FakeRegexCommandPlugin(self.config)
CONF.callsign = fake.FAKE_TO_CALLSIGN
p = fake.FakeRegexCommandPlugin()
packet = fake.fake_packet(message="f")
expected = fake.FAKE_MESSAGE_TEXT