mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-18 01:23:56 -04:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6257c9ea90 | |||
| b00c8db3d6 | |||
| 79270f95be | |||
| 29a60b7ed0 | |||
| e8100d8777 | |||
| 764730c123 | |||
| 610e40aecd | |||
| 2f6e7e17e8 | |||
| a7bbde4a43 | |||
| 7530bcf55c | |||
| ab37a5e7a7 | |||
| 3b9970c0e7 | |||
| e57a2e2ffc | |||
| 6a1cea63e4 | |||
| 592b328956 | |||
| 450bacfe99 | |||
| cd62db95c1 | |||
| 28b54c330d | |||
| 7c653cc100 | |||
| b7791eb4fa | |||
| 440c8d54ad | |||
| bcc1b4e309 | |||
| 8ea00e9888 | |||
| 5d6ac5cf31 | |||
| e0e75149a9 | |||
| a5184fb98c | |||
| 0ad791bdd9 | |||
| 96cc07d15f | |||
| d3dd08714b | |||
| 055835cb3c | |||
| ff8bf02e26 | |||
| b5b286e75c | |||
| 1233137caf | |||
| 1d5f76defc | |||
| 950c62f49b | |||
| 7aaa002a0e | |||
| e27887db1a | |||
| 5e50792e80 | |||
| deec249c45 | |||
| ade3c49e93 | |||
| 6fb610582d | |||
| bda2ef00dd | |||
| 446484e631 | |||
| a8a6b1aa07 | |||
| 8842fb1b44 | |||
| 152132b0ed | |||
| 7787dc1be4 | |||
| 10e34d8634 | |||
| 9469410929 | |||
| 998bc32c27 | |||
| 88db485eb4 | |||
| 5d17809895 | |||
| 059cc86a11 | |||
| ffdd1e47b2 | |||
| cdcb98e438 | |||
| 89727e2b8e | |||
| 617973f561 | |||
| 9187b9781a | |||
| 8287c09ce5 | |||
| 82def598f0 | |||
| 3463c6eb96 | |||
| 2ead6a97da | |||
| 7d0006b0a6 | |||
| 30df452e00 | |||
| 49f3ea8339 |
@@ -1,9 +1,99 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
v2.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
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
REQUIREMENTS_TXT ?= requirements.txt dev-requirements.txt
|
||||
WORKDIR?=.
|
||||
VENVDIR ?= $(WORKDIR)/.aprsd-venv
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
.PHONY: dev docs server test
|
||||
|
||||
include Makefile.venv
|
||||
Makefile.venv:
|
||||
curl \
|
||||
|
||||
+103
-85
@@ -46,56 +46,47 @@ 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.
|
||||
|
||||
|
||||
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
|
||||
* 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 │
|
||||
│ 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 │
|
||||
└────────────────────────┴────────────────────────────────────────────────────────────┴──────────────┴───────────────────────────────────────────┘
|
||||
|
||||
|
||||
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.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 │
|
||||
└──────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
|
||||
|
||||
|
||||
Installation:
|
||||
installation:
|
||||
=============
|
||||
|
||||
pip install aprsd
|
||||
@@ -112,20 +103,21 @@ Help
|
||||
└─[$] > 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
|
||||
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 This dumps the config to stdout.
|
||||
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.
|
||||
|
||||
|
||||
|
||||
@@ -145,8 +137,12 @@ Output
|
||||
|
||||
└─> 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'
|
||||
@@ -184,18 +180,38 @@ Output
|
||||
- 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]'
|
||||
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: aprsd
|
||||
admin: password-here
|
||||
ham:
|
||||
callsign: CALLSIGN
|
||||
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:
|
||||
@@ -229,35 +245,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
|
||||
@@ -269,25 +285,27 @@ 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.
|
||||
|
||||
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
#
|
||||
# Listen on amateur radio aprs-is network for messages and respond to them.
|
||||
# You must have an amateur radio callsign to use this software. You must
|
||||
# create an ~/.aprsd/config.yml file with all of the required settings. To
|
||||
# generate an example config.yml, just run aprsd, then copy the sample config
|
||||
# to ~/.aprsd/config.yml and edit the settings.
|
||||
#
|
||||
# APRS messages:
|
||||
# l(ocation) = descriptive location of calling station
|
||||
# w(eather) = temp, (hi/low) forecast, later forecast
|
||||
# t(ime) = respond with the current time
|
||||
# f(ortune) = respond with a short fortune
|
||||
# -email_addr email text = send an email
|
||||
# -2 = display the last 2 emails received
|
||||
# p(ing) = respond with Pong!/time
|
||||
# anything else = respond with usage
|
||||
#
|
||||
# (C)2018 Craig Lamparter
|
||||
# License GPLv2
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import cli_helper
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import messaging, packets, stats, threads, utils
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
flask_enabled = False
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
@click.group(context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
@click.pass_context
|
||||
def cli(ctx):
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
# First import all the possible commands for the CLI
|
||||
# The commands themselves live in the cmds directory
|
||||
from .cmds import ( # noqa
|
||||
completion, dev, healthcheck, list_plugins, listen, send_message,
|
||||
server,
|
||||
)
|
||||
cli()
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global flask_enabled
|
||||
|
||||
click.echo("signal_handler: called")
|
||||
threads.APRSDThreadList().stop_all()
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(1.5)
|
||||
messaging.MsgTrack().save()
|
||||
packets.WatchList().save()
|
||||
packets.SeenList().save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
if flask_enabled:
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options_no_config
|
||||
def check_version(ctx):
|
||||
"""Check this version against the latest in pypi.org."""
|
||||
level, msg = utils._check_version()
|
||||
if level:
|
||||
click.secho(msg, fg="yellow")
|
||||
else:
|
||||
click.secho(msg, fg="green")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def sample_config(ctx):
|
||||
"""This dumps the config to stdout."""
|
||||
click.echo(aprsd_config.dump_default_cfg())
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_context
|
||||
def version(ctx):
|
||||
"""Show the APRSD version."""
|
||||
click.echo(click.style("APRSD Version : ", fg="white"), nl=False)
|
||||
click.secho(f"{aprsd.__version__}", fg="yellow", bold=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
from functools import update_wrapper
|
||||
import typing as t
|
||||
|
||||
import click
|
||||
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import log
|
||||
|
||||
|
||||
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||
|
||||
common_options = [
|
||||
click.option(
|
||||
"--loglevel",
|
||||
default="INFO",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
),
|
||||
click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
),
|
||||
click.option(
|
||||
"--quiet",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Don't log to stdout",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def add_options(options):
|
||||
def _add_options(func):
|
||||
for option in reversed(options):
|
||||
func = option(func)
|
||||
return func
|
||||
return _add_options
|
||||
|
||||
|
||||
def process_standard_options(f: F) -> F:
|
||||
def new_func(*args, **kwargs):
|
||||
ctx = args[0]
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["loglevel"] = kwargs["loglevel"]
|
||||
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"],
|
||||
)
|
||||
|
||||
del kwargs["loglevel"]
|
||||
del kwargs["config_file"]
|
||||
del kwargs["quiet"]
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return update_wrapper(t.cast(F, new_func), f)
|
||||
|
||||
|
||||
def process_standard_options_no_config(f: F) -> F:
|
||||
"""Use this as a decorator when config isn't needed."""
|
||||
def new_func(*args, **kwargs):
|
||||
ctx = args[0]
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["loglevel"] = kwargs["loglevel"]
|
||||
ctx.obj["config_file"] = kwargs["config_file"]
|
||||
ctx.obj["quiet"] = kwargs["quiet"]
|
||||
log.setup_logging_no_config(
|
||||
ctx.obj["loglevel"],
|
||||
ctx.obj["quiet"],
|
||||
)
|
||||
|
||||
del kwargs["loglevel"]
|
||||
del kwargs["config_file"]
|
||||
del kwargs["quiet"]
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return update_wrapper(t.cast(F, new_func), f)
|
||||
@@ -131,6 +131,8 @@ class KISSClient(Client):
|
||||
if config.get("kiss.tcp.enabled", default=False):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def transport(config):
|
||||
if config.get("kiss.serial.enabled", default=False):
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
from aprsd.aprsd import cli
|
||||
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
@cli.group(help="Click Completion subcommands", context_settings=CONTEXT_SETTINGS)
|
||||
@click.pass_context
|
||||
def completion(ctx):
|
||||
pass
|
||||
|
||||
|
||||
# show dumps out the completion code for a particular shell
|
||||
@completion.command(help="Show completion code for shell", name="show")
|
||||
@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
|
||||
@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
# install will install the completion code for a particular shell
|
||||
@completion.command(help="Install completion code for a shell", name="install")
|
||||
@click.option("--append/--overwrite", help="Append the completion code to the file", default=None)
|
||||
@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
|
||||
@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
||||
shell, path = click_completion.core.install(shell=shell, path=path, append=append, extra_env=extra_env)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
@@ -0,0 +1,134 @@
|
||||
#
|
||||
# Dev.py is used to help develop plugins
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import logging
|
||||
|
||||
import click
|
||||
|
||||
# local imports here
|
||||
from aprsd import cli_helper, client, messaging, packets, plugin, stats, trace
|
||||
from aprsd.aprsd import cli
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
@cli.group(help="Development type subcommands", context_settings=CONTEXT_SETTINGS)
|
||||
@click.pass_context
|
||||
def dev(ctx):
|
||||
pass
|
||||
|
||||
|
||||
@dev.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from.",
|
||||
)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--plugin",
|
||||
"plugin_path",
|
||||
show_default=True,
|
||||
default=None,
|
||||
help="The plugin to run. Ex: aprsd.plugins.ping.PingPlugin",
|
||||
)
|
||||
@click.option(
|
||||
"-a",
|
||||
"--all",
|
||||
"load_all",
|
||||
show_default=True,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Load all the plugins in config?",
|
||||
)
|
||||
@click.option(
|
||||
"-n",
|
||||
"--num",
|
||||
"number",
|
||||
show_default=True,
|
||||
default=1,
|
||||
help="Number of times to call the plugin",
|
||||
)
|
||||
@click.argument("message", nargs=-1, required=True)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
def test_plugin(
|
||||
ctx,
|
||||
aprs_login,
|
||||
plugin_path,
|
||||
load_all,
|
||||
number,
|
||||
message,
|
||||
):
|
||||
"""Test an individual APRSD plugin given a python path."""
|
||||
config = ctx.obj["config"]
|
||||
|
||||
if not aprs_login:
|
||||
if not config.exists("aprs.login"):
|
||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||
ctx.exit(-1)
|
||||
return
|
||||
else:
|
||||
fromcall = config.get("aprs.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")
|
||||
return
|
||||
|
||||
if type(message) is tuple:
|
||||
message = " ".join(message)
|
||||
|
||||
if config["aprsd"].get("trace", False):
|
||||
trace.setup_tracing(["method", "api"])
|
||||
|
||||
client.Client(config)
|
||||
stats.APRSDStats(config)
|
||||
messaging.MsgTrack(config=config)
|
||||
packets.WatchList(config=config)
|
||||
packets.SeenList(config=config)
|
||||
|
||||
pm = plugin.PluginManager(config)
|
||||
if load_all:
|
||||
pm.setup_plugins()
|
||||
else:
|
||||
pm._init()
|
||||
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
||||
if not obj:
|
||||
click.echo(ctx.get_help())
|
||||
click.echo("")
|
||||
ctx.fail(f"Failed to create object from plugin path '{plugin_path}'")
|
||||
ctx.exit()
|
||||
|
||||
# Register the plugin they wanted tested.
|
||||
LOG.info(
|
||||
"Testing plugin {} Version {}".format(
|
||||
obj.__class__, obj.version,
|
||||
),
|
||||
)
|
||||
pm._pluggy_pm.register(obj)
|
||||
login = config["aprs"]["login"]
|
||||
|
||||
packet = {
|
||||
"from": fromcall, "addresse": login,
|
||||
"message_text": message,
|
||||
"format": "message",
|
||||
"msgNo": 1,
|
||||
}
|
||||
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
|
||||
|
||||
for x in range(number):
|
||||
reply = pm.run(packet)
|
||||
# Plugin might have threads, so lets stop them so we can exit.
|
||||
# obj.stop_threads()
|
||||
LOG.info(f"Result{x} = '{reply}'")
|
||||
pm.stop()
|
||||
@@ -0,0 +1,80 @@
|
||||
#
|
||||
# Used to fetch the stats url and determine if
|
||||
# aprsd server is 'healthy'
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import click
|
||||
import requests
|
||||
from rich.console import Console
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, utils
|
||||
# local imports here
|
||||
from aprsd.aprsd import cli
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
console = Console()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"--url",
|
||||
"health_url",
|
||||
show_default=True,
|
||||
default="http://localhost:8001/stats",
|
||||
help="The aprsd url to call for checking health/stats",
|
||||
)
|
||||
@click.option(
|
||||
"--timeout",
|
||||
show_default=True,
|
||||
default=3,
|
||||
help="How long to wait for healtcheck url to come back",
|
||||
)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options_no_config
|
||||
def healthcheck(ctx, health_url, timeout):
|
||||
"""Check the health of the running aprsd server."""
|
||||
console.log(f"APRSD HealthCheck version: {aprsd.__version__}")
|
||||
with console.status(f"APRSD HealthCheck version: {aprsd.__version__}") as status:
|
||||
try:
|
||||
status.update(f"Contacting APRSD on {health_url}")
|
||||
url = health_url
|
||||
response = requests.get(url, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
except Exception as ex:
|
||||
console.log(f"Failed to fetch healthcheck url '{url}' : '{ex}'")
|
||||
sys.exit(-1)
|
||||
else:
|
||||
status.update("Contacted APRSD. Parsing results.")
|
||||
stats = json.loads(response.text)
|
||||
email_thread_last_update = stats["stats"]["email"]["thread_last_update"]
|
||||
|
||||
if email_thread_last_update != "never":
|
||||
delta = utils.parse_delta_str(email_thread_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
console.log(f"Email thread is very old! {d}")
|
||||
sys.exit(-1)
|
||||
|
||||
aprsis_last_update = stats["stats"]["aprs-is"]["last_update"]
|
||||
delta = utils.parse_delta_str(aprsis_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error(f"APRS-IS last update is very old! {d}")
|
||||
sys.exit(-1)
|
||||
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,242 @@
|
||||
import fnmatch
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import re
|
||||
import sys
|
||||
from traceback import print_tb
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import click
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
from thesmuggler import smuggle
|
||||
|
||||
from aprsd import cli_helper
|
||||
from aprsd import plugin as aprsd_plugin
|
||||
from aprsd.aprsd import cli
|
||||
from aprsd.plugins import (
|
||||
email, fortune, location, notify, ping, query, time, version, weather,
|
||||
)
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
def onerror(name):
|
||||
print(f"Error importing module {name}")
|
||||
type, value, traceback = sys.exc_info()
|
||||
print_tb(traceback)
|
||||
|
||||
|
||||
def is_plugin(obj):
|
||||
for c in inspect.getmro(obj):
|
||||
if issubclass(c, aprsd_plugin.APRSDPluginBase):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def plugin_type(obj):
|
||||
for c in inspect.getmro(obj):
|
||||
if issubclass(c, aprsd_plugin.APRSDRegexCommandPluginBase):
|
||||
return "RegexCommand"
|
||||
if issubclass(c, aprsd_plugin.APRSDWatchListPluginBase):
|
||||
return "WatchList"
|
||||
if issubclass(c, aprsd_plugin.APRSDPluginBase):
|
||||
return "APRSDPluginBase"
|
||||
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def walk_package(package):
|
||||
return pkgutil.walk_packages(
|
||||
package.__path__,
|
||||
package.__name__ + ".",
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
|
||||
def get_module_info(package_name, module_name, module_path):
|
||||
if not os.path.exists(module_path):
|
||||
return None
|
||||
|
||||
dir_path = os.path.realpath(module_path)
|
||||
pattern = "*.py"
|
||||
|
||||
obj_list = []
|
||||
|
||||
for path, _subdirs, files in os.walk(dir_path):
|
||||
for name in files:
|
||||
if fnmatch.fnmatch(name, pattern):
|
||||
module = smuggle(f"{path}/{name}")
|
||||
for mem_name, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj) and is_plugin(obj):
|
||||
obj_list.append(
|
||||
{
|
||||
"package": package_name,
|
||||
"name": mem_name, "obj": obj,
|
||||
"version": obj.version,
|
||||
"path": f"{'.'.join([module_name, obj.__name__])}",
|
||||
},
|
||||
)
|
||||
|
||||
return obj_list
|
||||
|
||||
|
||||
def get_installed_plugins():
|
||||
# installed plugins
|
||||
ip = {}
|
||||
for finder, name, ispkg in pkgutil.iter_modules():
|
||||
if name.startswith("aprsd_"):
|
||||
if ispkg:
|
||||
module = importlib.import_module(name)
|
||||
pkgs = walk_package(module)
|
||||
for pkg in pkgs:
|
||||
pkg_info = get_module_info(module.__name__, pkg.name, module.__path__[0])
|
||||
ip[name] = pkg_info
|
||||
return ip
|
||||
|
||||
|
||||
def show_built_in_plugins(console):
|
||||
modules = [email, fortune, location, notify, ping, query, time, version, weather]
|
||||
plugins = []
|
||||
|
||||
for module in modules:
|
||||
entries = inspect.getmembers(module, inspect.isclass)
|
||||
for entry in entries:
|
||||
cls = entry[1]
|
||||
if issubclass(cls, aprsd_plugin.APRSDPluginBase):
|
||||
info = {
|
||||
"name": cls.__qualname__,
|
||||
"path": f"{cls.__module__}.{cls.__qualname__}",
|
||||
"version": cls.version,
|
||||
"docstring": cls.__doc__,
|
||||
"short_desc": cls.short_description,
|
||||
}
|
||||
|
||||
if issubclass(cls, aprsd_plugin.APRSDRegexCommandPluginBase):
|
||||
info["command_regex"] = cls.command_regex
|
||||
info["type"] = "RegexCommand"
|
||||
|
||||
if issubclass(cls, aprsd_plugin.APRSDWatchListPluginBase):
|
||||
info["type"] = "WatchList"
|
||||
|
||||
plugins.append(info)
|
||||
|
||||
plugins = sorted(plugins, key=lambda i: i["name"])
|
||||
|
||||
table = Table(
|
||||
title="[not italic]:snake:[/] [bold][magenta]APRSD Built-in Plugins [not italic]:snake:[/]",
|
||||
)
|
||||
table.add_column("Plugin Name", style="cyan", no_wrap=True)
|
||||
table.add_column("Info", style="bold yellow")
|
||||
table.add_column("Type", style="bold green")
|
||||
table.add_column("Plugin Path", style="bold blue")
|
||||
for entry in plugins:
|
||||
table.add_row(entry["name"], entry["short_desc"], entry["type"], entry["path"])
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
def show_pypi_plugins(installed_plugins, console):
|
||||
query = "aprsd"
|
||||
api_url = "https://pypi.org/search/"
|
||||
snippets = []
|
||||
s = requests.Session()
|
||||
for page in range(1, 3):
|
||||
params = {"q": query, "page": page}
|
||||
r = s.get(api_url, params=params)
|
||||
soup = BeautifulSoup(r.text, "html.parser")
|
||||
snippets += soup.select('a[class*="snippet"]')
|
||||
if not hasattr(s, "start_url"):
|
||||
s.start_url = r.url.rsplit("&page", maxsplit=1).pop(0)
|
||||
|
||||
title = Text.assemble(
|
||||
("Pypi.org APRSD Installable Plugin Packages\n\n", "bold magenta"),
|
||||
("Install any of the following plugins with ", "bold yellow"),
|
||||
("'pip install ", "bold white"),
|
||||
("<Plugin Package Name>'", "cyan"),
|
||||
)
|
||||
|
||||
table = Table(title=title)
|
||||
table.add_column("Plugin Package Name", style="cyan", no_wrap=True)
|
||||
table.add_column("Description", style="yellow")
|
||||
table.add_column("Version", style="yellow", justify="center")
|
||||
table.add_column("Released", style="bold green", justify="center")
|
||||
table.add_column("Installed?", style="red", justify="center")
|
||||
for snippet in snippets:
|
||||
link = urljoin(api_url, snippet.get("href"))
|
||||
package = re.sub(r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip())
|
||||
version = re.sub(r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip())
|
||||
released = re.sub(r"\s+", " ", snippet.select_one('span[class*="released"]').text.strip())
|
||||
description = re.sub(r"\s+", " ", snippet.select_one('p[class*="description"]').text.strip())
|
||||
emoji = ":open_file_folder:"
|
||||
|
||||
if "aprsd-" not in package or "-plugin" not in package:
|
||||
continue
|
||||
|
||||
under = package.replace("-", "_")
|
||||
if under in installed_plugins:
|
||||
installed = "Yes"
|
||||
else:
|
||||
installed = "No"
|
||||
|
||||
table.add_row(
|
||||
f"[link={link}]{emoji}[/link] {package}",
|
||||
description, version, released, installed,
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(table)
|
||||
return
|
||||
|
||||
|
||||
def show_installed_plugins(installed_plugins, console):
|
||||
if not installed_plugins:
|
||||
return
|
||||
|
||||
table = Table(
|
||||
title="[not italic]:snake:[/] [bold][magenta]APRSD Installed 3rd party Plugins [not italic]:snake:[/]",
|
||||
)
|
||||
table.add_column("Package Name", style=" bold white", no_wrap=True)
|
||||
table.add_column("Plugin Name", style="cyan", no_wrap=True)
|
||||
table.add_column("Version", style="yellow", justify="center")
|
||||
table.add_column("Type", style="bold green")
|
||||
table.add_column("Plugin Path", style="bold blue")
|
||||
for name in installed_plugins:
|
||||
for plugin in installed_plugins[name]:
|
||||
table.add_row(
|
||||
name.replace("_", "-"),
|
||||
plugin["name"],
|
||||
plugin["version"],
|
||||
plugin_type(plugin["obj"]),
|
||||
plugin["path"],
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(table)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options_no_config
|
||||
def list_plugins(ctx):
|
||||
"""List the built in plugins available to APRSD."""
|
||||
console = Console()
|
||||
|
||||
with console.status("Show Built-in Plugins") as status:
|
||||
show_built_in_plugins(console)
|
||||
|
||||
status.update("Fetching pypi.org plugins")
|
||||
installed_plugins = get_installed_plugins()
|
||||
show_pypi_plugins(installed_plugins, console)
|
||||
|
||||
status.update("Looking for installed APRSD plugins")
|
||||
show_installed_plugins(installed_plugins, console)
|
||||
@@ -0,0 +1,164 @@
|
||||
#
|
||||
# License GPLv2
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
import click
|
||||
from rich.console import Console
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import (
|
||||
cli_helper, client, messaging, packets, stats, threads, trace, utils,
|
||||
)
|
||||
from aprsd.aprsd import cli
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
console = Console()
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
threads.APRSDThreadList().stop_all()
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(5)
|
||||
LOG.info(stats.APRSDStats())
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-password",
|
||||
envvar="APRS_PASSWORD",
|
||||
show_envvar=True,
|
||||
help="the APRS-IS password for APRS_LOGIN",
|
||||
)
|
||||
@click.argument(
|
||||
"filter",
|
||||
nargs=-1,
|
||||
required=True,
|
||||
)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
def listen(
|
||||
ctx,
|
||||
aprs_login,
|
||||
aprs_password,
|
||||
filter,
|
||||
):
|
||||
"""Listen to packets on the APRS-IS Network based on FILTER.
|
||||
|
||||
FILTER is the APRS Filter to use.\n
|
||||
see http://www.aprs-is.net/javAPRSFilter.aspx\n
|
||||
r/lat/lon/dist - Range Filter Pass posits and objects within dist km from lat/lon.\n
|
||||
p/aa/bb/cc... - Prefix Filter Pass traffic with fromCall that start with aa or bb or cc.\n
|
||||
b/call1/call2... - Budlist Filter Pass all traffic from exact call: call1, call2, ... (* wild card allowed) \n
|
||||
o/obj1/obj2... - Object Filter Pass all objects with the exact name of obj1, obj2, ... (* wild card allowed)\n
|
||||
|
||||
"""
|
||||
config = ctx.obj["config"]
|
||||
|
||||
if not aprs_login:
|
||||
click.echo(ctx.get_help())
|
||||
click.echo("")
|
||||
ctx.fail("Must set --aprs_login or APRS_LOGIN")
|
||||
ctx.exit()
|
||||
|
||||
if not aprs_password:
|
||||
click.echo(ctx.get_help())
|
||||
click.echo("")
|
||||
ctx.fail("Must set --aprs-password or APRS_PASSWORD")
|
||||
ctx.exit()
|
||||
|
||||
config["aprs"]["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)
|
||||
|
||||
# Try and load saved MsgTrack list
|
||||
LOG.debug("Loading saved MsgTrack object.")
|
||||
messaging.MsgTrack(config=config).load()
|
||||
packets.WatchList(config=config).load()
|
||||
packets.SeenList(config=config).load()
|
||||
|
||||
@trace.trace
|
||||
def rx_packet(packet):
|
||||
resp = packet.get("response", None)
|
||||
if resp == "ack":
|
||||
ack_num = packet.get("msgNo")
|
||||
console.log(f"We saw an ACK {ack_num} Ignoring")
|
||||
messaging.log_packet(packet)
|
||||
else:
|
||||
message = packet.get("message_text", None)
|
||||
fromcall = packet["from"]
|
||||
msg_number = packet.get("msgNo", "0")
|
||||
messaging.log_message(
|
||||
"Received Message",
|
||||
packet["raw"],
|
||||
message,
|
||||
fromcall=fromcall,
|
||||
ack=msg_number,
|
||||
console=console,
|
||||
)
|
||||
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
client.ClientFactory.setup(config)
|
||||
# Make sure we have 1 client transport enabled
|
||||
if not client.factory.is_client_enabled():
|
||||
LOG.error("No Clients are enabled in config.")
|
||||
sys.exit(-1)
|
||||
|
||||
# Creates the client object
|
||||
LOG.info("Creating client connection")
|
||||
client.factory.create().client
|
||||
aprs_client = client.factory.create().client
|
||||
|
||||
LOG.debug(f"Filter by '{filter}'")
|
||||
aprs_client.set_filter(filter)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
with console.status("Listening for packets"):
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
except aprslib.exceptions.ConnectionDrop:
|
||||
LOG.error("Connection dropped, reconnecting")
|
||||
time.sleep(5)
|
||||
# Force the deletion of the client object connected to aprs
|
||||
# This will cause a reconnect, next time client.get_client()
|
||||
# is called
|
||||
aprs_client.reset()
|
||||
except aprslib.exceptions.UnknownFormat:
|
||||
LOG.error("Got a Bad packet")
|
||||
@@ -0,0 +1,169 @@
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from aprslib.exceptions import LoginError
|
||||
import click
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, client, messaging, packets
|
||||
from aprsd.aprsd import cli
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from. Defaults to config entry.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-password",
|
||||
envvar="APRS_PASSWORD",
|
||||
show_envvar=True,
|
||||
help="the APRS-IS password for APRS_LOGIN. Defaults to config entry.",
|
||||
)
|
||||
@click.option(
|
||||
"--no-ack",
|
||||
"-n",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Don't wait for an ack, just sent it to APRS-IS and bail.",
|
||||
)
|
||||
@click.option(
|
||||
"--wait-response",
|
||||
"-w",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Wait for a response to the message?",
|
||||
)
|
||||
@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
|
||||
@click.argument("tocallsign", required=True)
|
||||
@click.argument("command", nargs=-1, required=True)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
def send_message(
|
||||
ctx,
|
||||
aprs_login,
|
||||
aprs_password,
|
||||
no_ack,
|
||||
wait_response,
|
||||
raw,
|
||||
tocallsign,
|
||||
command,
|
||||
):
|
||||
"""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"):
|
||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||
ctx.exit(-1)
|
||||
return
|
||||
else:
|
||||
config["aprs"]["login"] = aprs_login
|
||||
|
||||
if not aprs_password:
|
||||
if not config.exists("aprs.password"):
|
||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||
ctx.exit(-1)
|
||||
return
|
||||
else:
|
||||
config["aprs"]["password"] = aprs_password
|
||||
|
||||
LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}")
|
||||
if type(command) is tuple:
|
||||
command = " ".join(command)
|
||||
if not quiet:
|
||||
if raw:
|
||||
LOG.info(f"L'{aprs_login}' R'{raw}'")
|
||||
else:
|
||||
LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'")
|
||||
|
||||
packets.PacketList(config=config)
|
||||
packets.WatchList(config=config)
|
||||
packets.SeenList(config=config)
|
||||
|
||||
got_ack = False
|
||||
got_response = False
|
||||
|
||||
def rx_packet(packet):
|
||||
global got_ack, got_response
|
||||
# LOG.debug("Got packet back {}".format(packet))
|
||||
resp = packet.get("response", None)
|
||||
if resp == "ack":
|
||||
ack_num = packet.get("msgNo")
|
||||
LOG.info(f"We got ack for our sent message {ack_num}")
|
||||
messaging.log_packet(packet)
|
||||
got_ack = True
|
||||
else:
|
||||
message = packet.get("message_text", None)
|
||||
fromcall = packet["from"]
|
||||
msg_number = packet.get("msgNo", "0")
|
||||
messaging.log_message(
|
||||
"Received Message",
|
||||
packet["raw"],
|
||||
message,
|
||||
fromcall=fromcall,
|
||||
ack=msg_number,
|
||||
)
|
||||
got_response = True
|
||||
# Send the ack back?
|
||||
ack = messaging.AckMessage(
|
||||
config["aprs"]["login"],
|
||||
fromcall,
|
||||
msg_id=msg_number,
|
||||
)
|
||||
ack.send_direct()
|
||||
|
||||
if got_ack:
|
||||
if wait_response:
|
||||
if got_response:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
client.ClientFactory.setup(config)
|
||||
client.factory.create().client
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
|
||||
# Send a message
|
||||
# then we setup a consumer to rx messages
|
||||
# We should get an ack back as well as a new message
|
||||
# we should bail after we get the ack and send an ack back for the
|
||||
# message
|
||||
if raw:
|
||||
msg = messaging.RawMessage(raw)
|
||||
msg.send_direct()
|
||||
sys.exit(0)
|
||||
else:
|
||||
msg = messaging.TextMessage(aprs_login, tocallsign, command)
|
||||
msg.send_direct()
|
||||
|
||||
if no_ack:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
aprs_client = client.factory.create().client
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
except aprslib.exceptions.ConnectionDrop:
|
||||
LOG.error("Connection dropped, reconnecting")
|
||||
time.sleep(5)
|
||||
# Force the deletion of the client object connected to aprs
|
||||
# This will cause a reconnect, next time client.get_client()
|
||||
# is called
|
||||
aprs_client.reset()
|
||||
@@ -0,0 +1,120 @@
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import click
|
||||
|
||||
import aprsd
|
||||
from aprsd import (
|
||||
cli_helper, client, flask, messaging, packets, plugin, stats, threads,
|
||||
trace, utils,
|
||||
)
|
||||
from aprsd import aprsd as aprsd_main
|
||||
from aprsd.aprsd import cli
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
# main() ###
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--flush",
|
||||
"flush",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Flush out all old aged messages on disk.",
|
||||
)
|
||||
@click.pass_context
|
||||
@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)
|
||||
else:
|
||||
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]}")
|
||||
|
||||
if config["aprsd"].get("trace", False):
|
||||
trace.setup_tracing(["method", "api"])
|
||||
stats.APRSDStats(config)
|
||||
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
client.ClientFactory.setup(config)
|
||||
# Make sure we have 1 client transport enabled
|
||||
if not client.factory.is_client_enabled():
|
||||
LOG.error("No Clients are enabled in config.")
|
||||
sys.exit(-1)
|
||||
|
||||
# Creates the client object
|
||||
LOG.info("Creating client connection")
|
||||
client.factory.create().client
|
||||
|
||||
# Now load the msgTrack from disk if any
|
||||
packets.PacketList(config=config)
|
||||
if flush:
|
||||
LOG.debug("Deleting saved MsgTrack.")
|
||||
messaging.MsgTrack(config=config).flush()
|
||||
packets.WatchList(config=config)
|
||||
packets.SeenList(config=config)
|
||||
else:
|
||||
# Try and load saved MsgTrack list
|
||||
LOG.debug("Loading saved MsgTrack object.")
|
||||
messaging.MsgTrack(config=config).load()
|
||||
packets.WatchList(config=config).load()
|
||||
packets.SeenList(config=config).load()
|
||||
|
||||
# Create the initial PM singleton and Register plugins
|
||||
LOG.info("Loading Plugin Manager and registering plugins")
|
||||
plugin_manager = plugin.PluginManager(config)
|
||||
plugin_manager.setup_plugins()
|
||||
|
||||
rx_thread = threads.APRSDRXThread(
|
||||
msg_queues=threads.msg_queues,
|
||||
config=config,
|
||||
)
|
||||
rx_thread.start()
|
||||
|
||||
messaging.MsgTrack().restart()
|
||||
|
||||
keepalive = threads.KeepAliveThread(config=config)
|
||||
keepalive.start()
|
||||
|
||||
web_enabled = config.get("aprsd.web.enabled", default=False)
|
||||
|
||||
if web_enabled:
|
||||
aprsd_main.flask_enabled = True
|
||||
(socketio, app) = flask.init_flask(config, loglevel, quiet)
|
||||
socketio.run(
|
||||
app,
|
||||
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
|
||||
+1
-6
@@ -79,6 +79,7 @@ DEFAULT_CONFIG_DICT = {
|
||||
"logformat": DEFAULT_LOG_FORMAT,
|
||||
"dateformat": DEFAULT_DATE_FORMAT,
|
||||
"save_location": DEFAULT_CONFIG_DIR,
|
||||
"rich_logging": False,
|
||||
"trace": False,
|
||||
"enabled_plugins": CORE_MESSAGE_PLUGINS,
|
||||
"units": "imperial",
|
||||
@@ -327,12 +328,6 @@ def parse_config(config_file):
|
||||
"ham.callsign",
|
||||
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
|
||||
)
|
||||
|
||||
check_option(
|
||||
config,
|
||||
["services", "aprs.fi", "apiKey"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"],
|
||||
)
|
||||
check_option(
|
||||
config,
|
||||
"aprs.login",
|
||||
|
||||
-251
@@ -1,251 +0,0 @@
|
||||
#
|
||||
# Dev.py is used to help develop plugins
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import sys
|
||||
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import client
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import plugin
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
f"{k:<12} {click_completion.core.shells[k]}"
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
|
||||
|
||||
# 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]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get(
|
||||
"logformat",
|
||||
aprsd_config.DEFAULT_LOG_FORMAT,
|
||||
)
|
||||
date_format = config["aprsd"].get(
|
||||
"dateformat",
|
||||
aprsd_config.DEFAULT_DATE_FORMAT,
|
||||
)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(
|
||||
log_file, maxBytes=(10248576 * 5),
|
||||
backupCount=4,
|
||||
)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="DEBUG",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--plugin",
|
||||
"plugin_path",
|
||||
show_default=True,
|
||||
default="aprsd.plugins.wx.WxPlugin",
|
||||
help="The plugin to run",
|
||||
)
|
||||
@click.option(
|
||||
"-a",
|
||||
"--all",
|
||||
"load_all",
|
||||
show_default=True,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Load all the plugins in config?",
|
||||
)
|
||||
@click.option(
|
||||
"-n",
|
||||
"--num",
|
||||
"number",
|
||||
show_default=True,
|
||||
default=1,
|
||||
help="Number of times to call the plugin",
|
||||
)
|
||||
@click.argument("fromcall")
|
||||
@click.argument("message", nargs=-1, required=True)
|
||||
def test_plugin(
|
||||
loglevel,
|
||||
config_file,
|
||||
plugin_path,
|
||||
load_all,
|
||||
number,
|
||||
fromcall,
|
||||
message,
|
||||
):
|
||||
"""APRSD Plugin test app."""
|
||||
|
||||
config = aprsd_config.parse_config(config_file)
|
||||
setup_logging(config, loglevel, False)
|
||||
|
||||
LOG.info(f"Test APRSD Plgin version: {aprsd.__version__}")
|
||||
if type(message) is tuple:
|
||||
message = " ".join(message)
|
||||
client.Client(config)
|
||||
|
||||
pm = plugin.PluginManager(config)
|
||||
if load_all:
|
||||
pm.setup_plugins()
|
||||
else:
|
||||
pm._init()
|
||||
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
||||
# Register the plugin they wanted tested.
|
||||
LOG.info(
|
||||
"Testing plugin {} Version {}".format(
|
||||
obj.__class__, obj.version,
|
||||
),
|
||||
)
|
||||
pm._pluggy_pm.register(obj)
|
||||
login = config["aprs"]["login"]
|
||||
|
||||
packet = {
|
||||
"from": fromcall, "addresse": login,
|
||||
"message_text": message,
|
||||
"format": "message",
|
||||
"msgNo": 1,
|
||||
}
|
||||
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
|
||||
|
||||
for x in range(number):
|
||||
reply = pm.run(packet)
|
||||
# Plugin might have threads, so lets stop them so we can exit.
|
||||
# obj.stop_threads()
|
||||
LOG.info(f"Result{x} = '{reply}'")
|
||||
pm.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,84 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import socketserver
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aprsd import utils
|
||||
|
||||
|
||||
# command line args
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--loglevel",
|
||||
default="DEBUG",
|
||||
choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
parser.add_argument("--quiet", action="store_true", help="Don't log to stdout")
|
||||
|
||||
parser.add_argument("--port", default=9099, type=int, help="The port to listen on .")
|
||||
parser.add_argument("--ip", default="127.0.0.1", help="The IP to listen on ")
|
||||
|
||||
CONFIG = None
|
||||
LOG = logging.getLogger("ARPSSERVER")
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(args):
|
||||
global LOG
|
||||
levels = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
log_level = levels[args.loglevel]
|
||||
|
||||
LOG.setLevel(log_level)
|
||||
log_format = "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]" " %(message)s"
|
||||
date_format = "%m/%d/%Y %I:%M:%S %p"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = RotatingFileHandler("aprs-server.log", maxBytes=(10248576 * 5), backupCount=4)
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not args.quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
|
||||
|
||||
class MyAPRSTCPHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
# self.request is the TCP socket connected to the client
|
||||
self.data = self.request.recv(1024).strip()
|
||||
LOG.debug(f"{self.client_address[0]} wrote:")
|
||||
LOG.debug(self.data)
|
||||
# just send back the same data, but upper-cased
|
||||
self.request.sendall(self.data.upper())
|
||||
|
||||
|
||||
def main():
|
||||
global CONFIG
|
||||
args = parser.parse_args()
|
||||
setup_logging(args)
|
||||
LOG.info("Test APRS server starting.")
|
||||
time.sleep(1)
|
||||
|
||||
CONFIG = utils.parse_config(args)
|
||||
|
||||
ip = CONFIG["aprs"]["host"]
|
||||
port = CONFIG["aprs"]["port"]
|
||||
LOG.info(f"Start server listening on {args.ip}:{args.port}")
|
||||
|
||||
with socketserver.TCPServer((ip, port), MyAPRSTCPHandler) as server:
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+35
-24
@@ -1,9 +1,7 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
@@ -11,6 +9,7 @@ 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
|
||||
@@ -19,8 +18,9 @@ from werkzeug.security import check_password_hash, generate_password_hash
|
||||
import aprsd
|
||||
from aprsd import client
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import messaging, packets, plugin, stats, threads, utils
|
||||
from aprsd import log, messaging, packets, plugin, stats, threads, utils
|
||||
from aprsd.clients import aprsis
|
||||
from aprsd.logging import logging as aprsd_logging
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -500,7 +500,7 @@ class LogMonitorThread(threads.APRSDThread):
|
||||
def loop(self):
|
||||
global socketio
|
||||
try:
|
||||
record = threads.logging_queue.get(block=True, timeout=5)
|
||||
record = log.logging_queue.get(block=True, timeout=5)
|
||||
json_record = self.json_record(record)
|
||||
socketio.emit(
|
||||
"log_entry", json_record,
|
||||
@@ -552,6 +552,15 @@ class LoggingNamespace(Namespace):
|
||||
|
||||
def setup_logging(config, 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]
|
||||
flask_log.setLevel(log_level)
|
||||
date_format = config["aprsd"].get(
|
||||
"dateformat",
|
||||
aprsd_config.DEFAULT_DATE_FORMAT,
|
||||
)
|
||||
|
||||
if not config["aprsd"]["web"].get("logging_enabled", False):
|
||||
# disable web logging
|
||||
@@ -559,28 +568,30 @@ def setup_logging(config, flask_app, loglevel, quiet):
|
||||
flask_app.logger.disabled = True
|
||||
return
|
||||
|
||||
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
if config["aprsd"].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(
|
||||
show_thread=True, thread_width=15,
|
||||
rich_tracebacks=True, omit_repeated_times=False,
|
||||
)
|
||||
rh.setFormatter(log_formatter)
|
||||
flask_log.addHandler(rh)
|
||||
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
for handler in flask_app.logger.handlers:
|
||||
handler.setFormatter(log_formatter)
|
||||
print(handler)
|
||||
|
||||
flask_log.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
flask_log.addHandler(sh)
|
||||
log_format = config["aprsd"].get(
|
||||
"logformat",
|
||||
aprsd_config.DEFAULT_LOG_FORMAT,
|
||||
)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = RotatingFileHandler(
|
||||
log_file, maxBytes=(10248576 * 5),
|
||||
backupCount=4,
|
||||
)
|
||||
fh.setFormatter(log_formatter)
|
||||
flask_log.addHandler(fh)
|
||||
|
||||
|
||||
def init_flask(config, loglevel, quiet):
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
#
|
||||
# Used to fetch the stats url and determine if
|
||||
# aprsd server is 'healthy'
|
||||
#
|
||||
#
|
||||
# python included libs
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import click
|
||||
import click_completion
|
||||
import requests
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import config as aprsd_config
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
LOG_LEVELS = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
}
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
f"{k:<12} {click_completion.core.shells[k]}"
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
|
||||
|
||||
# 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 = LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s"
|
||||
date_format = "%m/%d/%Y %I:%M:%S %p"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprs"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
|
||||
|
||||
def parse_delta_str(s):
|
||||
if "day" in s:
|
||||
m = re.match(
|
||||
r"(?P<days>[-\d]+) day[s]*, (?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)",
|
||||
s,
|
||||
)
|
||||
else:
|
||||
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
|
||||
return {key: float(val) for key, val in m.groupdict().items()}
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="INFO",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"--url",
|
||||
"health_url",
|
||||
show_default=True,
|
||||
default="http://localhost:8001/stats",
|
||||
help="The aprsd url to call for checking health/stats",
|
||||
)
|
||||
@click.option(
|
||||
"--timeout",
|
||||
show_default=True,
|
||||
default=3,
|
||||
help="How long to wait for healtcheck url to come back",
|
||||
)
|
||||
def check(loglevel, config_file, health_url, timeout):
|
||||
"""APRSD Plugin test app."""
|
||||
|
||||
config = aprsd_config.parse_config(config_file)
|
||||
|
||||
setup_logging(config, loglevel, False)
|
||||
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")
|
||||
|
||||
try:
|
||||
url = health_url
|
||||
response = requests.get(url, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to fetch healthcheck url '{url}' : '{ex}'")
|
||||
sys.exit(-1)
|
||||
else:
|
||||
stats = json.loads(response.text)
|
||||
LOG.debug(stats)
|
||||
|
||||
email_thread_last_update = stats["stats"]["email"]["thread_last_update"]
|
||||
|
||||
delta = parse_delta_str(email_thread_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error(f"Email thread is very old! {d}")
|
||||
sys.exit(-1)
|
||||
|
||||
aprsis_last_update = stats["stats"]["aprs-is"]["last_update"]
|
||||
delta = parse_delta_str(aprsis_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.error(f"APRS-IS last update is very old! {d}")
|
||||
sys.exit(-1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
-394
@@ -1,394 +0,0 @@
|
||||
#
|
||||
# Listen on amateur radio aprs-is network for messages and respond to them.
|
||||
# You must have an amateur radio callsign to use this software. You must
|
||||
# create an ~/.aprsd/config.yml file with all of the required settings. To
|
||||
# generate an example config.yml, just run aprsd, then copy the sample config
|
||||
# to ~/.aprsd/config.yml and edit the settings.
|
||||
#
|
||||
# APRS messages:
|
||||
# l(ocation) = descriptive location of calling station
|
||||
# w(eather) = temp, (hi/low) forecast, later forecast
|
||||
# t(ime) = respond with the current time
|
||||
# f(ortune) = respond with a short fortune
|
||||
# -email_addr email text = send an email
|
||||
# -2 = display the last 2 emails received
|
||||
# p(ing) = respond with Pong!/time
|
||||
# anything else = respond with usage
|
||||
#
|
||||
# (C)2018 Craig Lamparter
|
||||
# License GPLv2
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from aprslib.exceptions import LoginError
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import client
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import messaging, stats, threads, trace, utils
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
flask_enabled = False
|
||||
|
||||
# server_event = threading.Event()
|
||||
|
||||
# localization, please edit:
|
||||
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
||||
# USER = "KM6XXX-9" # callsign of this aprs client with SSID
|
||||
# PASS = "99999" # google how to generate this
|
||||
# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email
|
||||
# shortcuts = {
|
||||
# "aa" : "5551239999@vtext.com",
|
||||
# "cl" : "craiglamparter@somedomain.org",
|
||||
# "wb" : "5553909472@vtext.com"
|
||||
# }
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
f"{k:<12} {click_completion.core.shells[k]}"
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global flask_enabled
|
||||
|
||||
threads.APRSDThreadList().stop_all()
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(5)
|
||||
tracker = messaging.MsgTrack()
|
||||
tracker.save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
if flask_enabled:
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
# 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]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
imap_logger = None
|
||||
if config["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][
|
||||
"imap"
|
||||
].get("debug", False):
|
||||
|
||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||
imap_logger.setLevel(log_level)
|
||||
imap_logger.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
if imap_logger:
|
||||
imap_logger.addHandler(sh)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="DEBUG",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-password",
|
||||
envvar="APRS_PASSWORD",
|
||||
show_envvar=True,
|
||||
help="the APRS-IS password for APRS_LOGIN",
|
||||
)
|
||||
@click.option(
|
||||
"--no-ack",
|
||||
"-n",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Don't wait for an ack, just sent it to APRS-IS and bail.",
|
||||
)
|
||||
@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
|
||||
@click.argument("tocallsign", required=False)
|
||||
@click.argument("command", nargs=-1, required=False)
|
||||
def listen(
|
||||
loglevel,
|
||||
quiet,
|
||||
config_file,
|
||||
aprs_login,
|
||||
aprs_password,
|
||||
no_ack,
|
||||
raw,
|
||||
tocallsign,
|
||||
command,
|
||||
):
|
||||
"""Send a message to a callsign via APRS_IS."""
|
||||
global got_ack, got_response
|
||||
|
||||
config = aprsd_config.parse_config(config_file)
|
||||
if not aprs_login:
|
||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||
return
|
||||
|
||||
if not aprs_password:
|
||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||
return
|
||||
|
||||
config["aprs"]["login"] = aprs_login
|
||||
config["aprs"]["password"] = aprs_password
|
||||
messaging.CONFIG = config
|
||||
|
||||
setup_logging(config, loglevel, quiet)
|
||||
LOG.info(f"APRSD TEST Started version: {aprsd.__version__}")
|
||||
if type(command) is tuple:
|
||||
command = " ".join(command)
|
||||
if not quiet:
|
||||
if raw:
|
||||
LOG.info(f"L'{aprs_login}' R'{raw}'")
|
||||
else:
|
||||
LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'")
|
||||
|
||||
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]}")
|
||||
|
||||
got_ack = False
|
||||
got_response = False
|
||||
|
||||
# TODO(walt) - manually edit this list
|
||||
# prior to running aprsd-listen listen
|
||||
watch_list = ["k*"]
|
||||
|
||||
# build last seen list
|
||||
last_seen = {}
|
||||
for callsign in watch_list:
|
||||
call = callsign.replace("*", "")
|
||||
last_seen[call] = datetime.datetime.now()
|
||||
|
||||
LOG.debug("Last seen list")
|
||||
LOG.debug(last_seen)
|
||||
|
||||
@trace.trace
|
||||
def rx_packet(packet):
|
||||
global got_ack, got_response
|
||||
LOG.debug("Got packet back {}".format(packet["raw"]))
|
||||
|
||||
if packet["from"] in last_seen:
|
||||
now = datetime.datetime.now()
|
||||
age = str(now - last_seen[packet["from"]])
|
||||
|
||||
delta = utils.parse_delta_str(age)
|
||||
d = datetime.timedelta(**delta)
|
||||
|
||||
max_timeout = {
|
||||
"seconds": config["aprsd"]["watch_list"]["alert_time_seconds"],
|
||||
}
|
||||
max_delta = datetime.timedelta(**max_timeout)
|
||||
if d > max_delta:
|
||||
LOG.debug(
|
||||
"NOTIFY!!! {} last seen {} max age={}".format(
|
||||
packet["from"],
|
||||
age,
|
||||
max_delta,
|
||||
),
|
||||
)
|
||||
else:
|
||||
LOG.debug(f"Not old enough to notify {d} < {max_delta}")
|
||||
LOG.debug("Update last seen from {}".format(packet["from"]))
|
||||
last_seen[packet["from"]] = now
|
||||
else:
|
||||
LOG.debug(
|
||||
"ignoring packet because {} not in watch list".format(packet["from"]),
|
||||
)
|
||||
|
||||
resp = packet.get("response", None)
|
||||
if resp == "ack":
|
||||
ack_num = packet.get("msgNo")
|
||||
LOG.info(f"We saw an ACK {ack_num} Ignoring")
|
||||
# messaging.log_packet(packet)
|
||||
got_ack = True
|
||||
else:
|
||||
message = packet.get("message_text", None)
|
||||
fromcall = packet["from"]
|
||||
msg_number = packet.get("msgNo", "0")
|
||||
messaging.log_message(
|
||||
"Received Message",
|
||||
packet["raw"],
|
||||
message,
|
||||
fromcall=fromcall,
|
||||
ack=msg_number,
|
||||
)
|
||||
|
||||
try:
|
||||
cl = client.Client(config)
|
||||
cl.setup_connection()
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
|
||||
aprs_client = client.get_client()
|
||||
|
||||
# filter_str = 'b/{}'.format('/'.join(watch_list))
|
||||
# LOG.debug("Filter by '{}'".format(filter_str))
|
||||
# aprs_client.set_filter(filter_str)
|
||||
filter_str = "p/{}".format("/".join(watch_list))
|
||||
LOG.debug(f"Filter by '{filter_str}'")
|
||||
aprs_client.set_filter(filter_str)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
except aprslib.exceptions.ConnectionDrop:
|
||||
LOG.error("Connection dropped, reconnecting")
|
||||
time.sleep(5)
|
||||
# Force the deletion of the client object connected to aprs
|
||||
# This will cause a reconnect, next time client.get_client()
|
||||
# is called
|
||||
cl.reset()
|
||||
except aprslib.exceptions.UnknownFormat:
|
||||
LOG.error("Got a shitty packet")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,81 @@
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import queue
|
||||
import sys
|
||||
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd.logging import logging as aprsd_logging
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
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]
|
||||
LOG.setLevel(log_level)
|
||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||
|
||||
rich_logging = False
|
||||
if config["aprsd"].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(
|
||||
show_thread=True, thread_width=20,
|
||||
rich_tracebacks=True, omit_repeated_times=False,
|
||||
)
|
||||
rh.setFormatter(log_formatter)
|
||||
LOG.addHandler(rh)
|
||||
rich_logging = True
|
||||
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
|
||||
if log_file:
|
||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
imap_logger = None
|
||||
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
|
||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||
imap_logger.setLevel(log_level)
|
||||
imap_logger.addHandler(fh)
|
||||
|
||||
if config.get("aprsd.web.enabled", default=False):
|
||||
qh = logging.handlers.QueueHandler(logging_queue)
|
||||
q_log_formatter = logging.Formatter(
|
||||
fmt=aprsd_config.QUEUE_LOG_FORMAT,
|
||||
datefmt=aprsd_config.QUEUE_DATE_FORMAT,
|
||||
)
|
||||
qh.setFormatter(q_log_formatter)
|
||||
LOG.addHandler(qh)
|
||||
|
||||
if not quiet and not rich_logging:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
if imap_logger:
|
||||
imap_logger.addHandler(sh)
|
||||
|
||||
|
||||
def setup_logging_no_config(loglevel, quiet):
|
||||
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = aprsd_config.DEFAULT_LOG_FORMAT
|
||||
date_format = aprsd_config.DEFAULT_DATE_FORMAT
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
@@ -0,0 +1,160 @@
|
||||
from datetime import datetime
|
||||
from logging import LogRecord
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Union
|
||||
|
||||
from rich._log_render import LogRender
|
||||
from rich.logging import RichHandler
|
||||
from rich.text import Text, TextType
|
||||
from rich.traceback import Traceback
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.console import Console, ConsoleRenderable, RenderableType
|
||||
from rich.table import Table
|
||||
|
||||
from aprsd import utils
|
||||
|
||||
|
||||
FormatTimeCallable = Callable[[datetime], Text]
|
||||
|
||||
|
||||
class APRSDRichLogRender(LogRender):
|
||||
|
||||
def __init__(
|
||||
self, *args,
|
||||
show_thread: bool = False,
|
||||
thread_width: Optional[int] = 10,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.show_thread = show_thread
|
||||
self.thread_width = thread_width
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
console: "Console",
|
||||
renderables: Iterable["ConsoleRenderable"],
|
||||
log_time: Optional[datetime] = None,
|
||||
time_format: Optional[Union[str, FormatTimeCallable]] = None,
|
||||
level: TextType = "",
|
||||
path: Optional[str] = None,
|
||||
line_no: Optional[int] = None,
|
||||
link_path: Optional[str] = None,
|
||||
thread_name: Optional[str] = None,
|
||||
) -> "Table":
|
||||
from rich.containers import Renderables
|
||||
from rich.table import Table
|
||||
|
||||
output = Table.grid(padding=(0, 1))
|
||||
output.expand = True
|
||||
if self.show_time:
|
||||
output.add_column(style="log.time")
|
||||
if self.show_thread:
|
||||
rgb = str(utils.rgb_from_name(thread_name)).replace(" ", "")
|
||||
output.add_column(style=f"rgb{rgb}", width=self.thread_width)
|
||||
if self.show_level:
|
||||
output.add_column(style="log.level", width=self.level_width)
|
||||
output.add_column(ratio=1, style="log.message", overflow="fold")
|
||||
if self.show_path and path:
|
||||
output.add_column(style="log.path")
|
||||
row: List["RenderableType"] = []
|
||||
if self.show_time:
|
||||
log_time = log_time or console.get_datetime()
|
||||
time_format = time_format or self.time_format
|
||||
if callable(time_format):
|
||||
log_time_display = time_format(log_time)
|
||||
else:
|
||||
log_time_display = Text(log_time.strftime(time_format))
|
||||
if log_time_display == self._last_time and self.omit_repeated_times:
|
||||
row.append(Text(" " * len(log_time_display)))
|
||||
else:
|
||||
row.append(log_time_display)
|
||||
self._last_time = log_time_display
|
||||
if self.show_thread:
|
||||
row.append(thread_name)
|
||||
if self.show_level:
|
||||
row.append(level)
|
||||
|
||||
row.append(Renderables(renderables))
|
||||
if self.show_path and path:
|
||||
path_text = Text()
|
||||
path_text.append(
|
||||
path, style=f"link file://{link_path}" if link_path else "",
|
||||
)
|
||||
if line_no:
|
||||
path_text.append(":")
|
||||
path_text.append(
|
||||
f"{line_no}",
|
||||
style=f"link file://{link_path}#{line_no}" if link_path else "",
|
||||
)
|
||||
row.append(path_text)
|
||||
|
||||
output.add_row(*row)
|
||||
return output
|
||||
|
||||
|
||||
class APRSDRichHandler(RichHandler):
|
||||
"""APRSD's extension of rich's RichHandler to show threads.
|
||||
|
||||
show_thread (bool, optional): Show the name of the thread in log entry. Defaults to False.
|
||||
thread_width (int, optional): The number of characters to show for thread name. Defaults to 10.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *args,
|
||||
show_thread: bool = True,
|
||||
thread_width: Optional[int] = 10,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.show_thread = show_thread
|
||||
self.thread_width = thread_width
|
||||
kwargs["show_thread"] = show_thread
|
||||
kwargs["thread_width"] = thread_width
|
||||
self._log_render = APRSDRichLogRender(
|
||||
show_time=True,
|
||||
show_level=True,
|
||||
show_path=True,
|
||||
omit_repeated_times=False,
|
||||
level_width=None,
|
||||
show_thread=show_thread,
|
||||
thread_width=thread_width,
|
||||
)
|
||||
|
||||
def render(
|
||||
self, *, record: LogRecord,
|
||||
traceback: Optional[Traceback],
|
||||
message_renderable: "ConsoleRenderable",
|
||||
) -> "ConsoleRenderable":
|
||||
"""Render log for display.
|
||||
|
||||
Args:
|
||||
record (LogRecord): logging Record.
|
||||
traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
|
||||
message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
|
||||
|
||||
Returns:
|
||||
ConsoleRenderable: Renderable to display log.
|
||||
"""
|
||||
path = Path(record.pathname).name
|
||||
level = self.get_level_text(record)
|
||||
time_format = None if self.formatter is None else self.formatter.datefmt
|
||||
log_time = datetime.fromtimestamp(record.created)
|
||||
thread_name = record.threadName
|
||||
|
||||
log_renderable = self._log_render(
|
||||
self.console,
|
||||
[message_renderable] if not traceback else [
|
||||
message_renderable,
|
||||
traceback,
|
||||
],
|
||||
log_time=log_time,
|
||||
time_format=time_format,
|
||||
level=level,
|
||||
path=path,
|
||||
line_no=record.lineno,
|
||||
link_path=record.pathname if self.enable_link_path else None,
|
||||
thread_name=thread_name,
|
||||
)
|
||||
return log_renderable
|
||||
-520
@@ -1,520 +0,0 @@
|
||||
#
|
||||
# Listen on amateur radio aprs-is network for messages and respond to them.
|
||||
# You must have an amateur radio callsign to use this software. You must
|
||||
# create an ~/.aprsd/config.yml file with all of the required settings. To
|
||||
# generate an example config.yml, just run aprsd, then copy the sample config
|
||||
# to ~/.aprsd/config.yml and edit the settings.
|
||||
#
|
||||
# APRS messages:
|
||||
# l(ocation) = descriptive location of calling station
|
||||
# w(eather) = temp, (hi/low) forecast, later forecast
|
||||
# t(ime) = respond with the current time
|
||||
# f(ortune) = respond with a short fortune
|
||||
# -email_addr email text = send an email
|
||||
# -2 = display the last 2 emails received
|
||||
# p(ing) = respond with Pong!/time
|
||||
# anything else = respond with usage
|
||||
#
|
||||
# (C)2018 Craig Lamparter
|
||||
# License GPLv2
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from aprslib.exceptions import LoginError
|
||||
import click
|
||||
import click_completion
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import (
|
||||
flask, messaging, packets, plugin, stats, threads, trace, utils,
|
||||
)
|
||||
from aprsd import client
|
||||
from aprsd import config as aprsd_config
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
flask_enabled = False
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""A custom completion match that supports case insensitive matching."""
|
||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
||||
string = string.lower()
|
||||
incomplete = incomplete.lower()
|
||||
return string.startswith(incomplete)
|
||||
|
||||
|
||||
click_completion.core.startswith = custom_startswith
|
||||
click_completion.init()
|
||||
|
||||
|
||||
cmd_help = """Shell completion for click-completion-command
|
||||
Available shell types:
|
||||
\b
|
||||
%s
|
||||
Default type: auto
|
||||
""" % "\n ".join(
|
||||
f"{k:<12} {click_completion.core.shells[k]}"
|
||||
for k in sorted(click_completion.core.shells.keys())
|
||||
)
|
||||
|
||||
|
||||
@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
def main():
|
||||
pass
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--append/--overwrite",
|
||||
help="Append the completion code to the file",
|
||||
default=None,
|
||||
)
|
||||
@click.option(
|
||||
"-i",
|
||||
"--case-insensitive/--no-case-insensitive",
|
||||
help="Case insensitive completion",
|
||||
)
|
||||
@click.argument(
|
||||
"shell",
|
||||
required=False,
|
||||
type=click_completion.DocumentedChoice(click_completion.core.shells),
|
||||
)
|
||||
@click.argument("path", required=False)
|
||||
def install(append, case_insensitive, shell, path):
|
||||
"""Install the click-completion-command completion"""
|
||||
extra_env = (
|
||||
{"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"}
|
||||
if case_insensitive
|
||||
else {}
|
||||
)
|
||||
shell, path = click_completion.core.install(
|
||||
shell=shell,
|
||||
path=path,
|
||||
append=append,
|
||||
extra_env=extra_env,
|
||||
)
|
||||
click.echo(f"{shell} completion installed in {path}")
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global flask_enabled
|
||||
|
||||
threads.APRSDThreadList().stop_all()
|
||||
if "subprocess" not in str(frame):
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
|
||||
datetime.datetime.now(),
|
||||
),
|
||||
)
|
||||
time.sleep(1.5)
|
||||
messaging.MsgTrack().save()
|
||||
packets.WatchList().save()
|
||||
packets.SeenList().save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
if flask_enabled:
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
# 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]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
log_file = config["aprsd"].get("logfile", None)
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
else:
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
imap_logger = None
|
||||
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
|
||||
|
||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||
imap_logger.setLevel(log_level)
|
||||
imap_logger.addHandler(fh)
|
||||
|
||||
if config.get("aprsd.web.enabled", default=False):
|
||||
qh = logging.handlers.QueueHandler(threads.logging_queue)
|
||||
q_log_formatter = logging.Formatter(
|
||||
fmt=aprsd_config.QUEUE_LOG_FORMAT,
|
||||
datefmt=aprsd_config.QUEUE_DATE_FORMAT,
|
||||
)
|
||||
qh.setFormatter(q_log_formatter)
|
||||
LOG.addHandler(qh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
if imap_logger:
|
||||
imap_logger.addHandler(sh)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="INFO",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
def check_version(loglevel, config_file):
|
||||
config = aprsd_config.parse_config(config_file)
|
||||
|
||||
setup_logging(config, loglevel, False)
|
||||
level, msg = utils._check_version()
|
||||
if level:
|
||||
LOG.warning(msg)
|
||||
else:
|
||||
LOG.info(msg)
|
||||
|
||||
|
||||
@main.command()
|
||||
def sample_config():
|
||||
"""This dumps the config to stdout."""
|
||||
click.echo(aprsd_config.dump_default_cfg())
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="DEBUG",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-login",
|
||||
envvar="APRS_LOGIN",
|
||||
show_envvar=True,
|
||||
help="What callsign to send the message from.",
|
||||
)
|
||||
@click.option(
|
||||
"--aprs-password",
|
||||
envvar="APRS_PASSWORD",
|
||||
show_envvar=True,
|
||||
help="the APRS-IS password for APRS_LOGIN",
|
||||
)
|
||||
@click.option(
|
||||
"--no-ack",
|
||||
"-n",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Don't wait for an ack, just sent it to APRS-IS and bail.",
|
||||
)
|
||||
@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
|
||||
@click.argument("tocallsign", required=False)
|
||||
@click.argument("command", nargs=-1, required=False)
|
||||
def send_message(
|
||||
loglevel,
|
||||
quiet,
|
||||
config_file,
|
||||
aprs_login,
|
||||
aprs_password,
|
||||
no_ack,
|
||||
raw,
|
||||
tocallsign,
|
||||
command,
|
||||
):
|
||||
"""Send a message to a callsign via APRS_IS."""
|
||||
global got_ack, got_response
|
||||
|
||||
config = aprsd_config.parse_config(config_file)
|
||||
if not aprs_login:
|
||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||
return
|
||||
|
||||
if not aprs_password:
|
||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||
return
|
||||
|
||||
config["aprs"]["login"] = aprs_login
|
||||
config["aprs"]["password"] = aprs_password
|
||||
messaging.CONFIG = config
|
||||
|
||||
setup_logging(config, loglevel, quiet)
|
||||
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
||||
if type(command) is tuple:
|
||||
command = " ".join(command)
|
||||
if not quiet:
|
||||
if raw:
|
||||
LOG.info(f"L'{aprs_login}' R'{raw}'")
|
||||
else:
|
||||
LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'")
|
||||
|
||||
got_ack = False
|
||||
got_response = False
|
||||
|
||||
def rx_packet(packet):
|
||||
global got_ack, got_response
|
||||
# LOG.debug("Got packet back {}".format(packet))
|
||||
resp = packet.get("response", None)
|
||||
if resp == "ack":
|
||||
ack_num = packet.get("msgNo")
|
||||
LOG.info(f"We got ack for our sent message {ack_num}")
|
||||
messaging.log_packet(packet)
|
||||
got_ack = True
|
||||
else:
|
||||
message = packet.get("message_text", None)
|
||||
fromcall = packet["from"]
|
||||
msg_number = packet.get("msgNo", "0")
|
||||
messaging.log_message(
|
||||
"Received Message",
|
||||
packet["raw"],
|
||||
message,
|
||||
fromcall=fromcall,
|
||||
ack=msg_number,
|
||||
)
|
||||
got_response = True
|
||||
# Send the ack back?
|
||||
ack = messaging.AckMessage(
|
||||
config["aprs"]["login"],
|
||||
fromcall,
|
||||
msg_id=msg_number,
|
||||
)
|
||||
ack.send_direct()
|
||||
|
||||
if got_ack and got_response:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
client.ClientFactory.setup(config)
|
||||
client.factory.create().client
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
|
||||
packets.PacketList(config=config)
|
||||
packets.WatchList(config=config)
|
||||
|
||||
# Send a message
|
||||
# then we setup a consumer to rx messages
|
||||
# We should get an ack back as well as a new message
|
||||
# we should bail after we get the ack and send an ack back for the
|
||||
# message
|
||||
if raw:
|
||||
msg = messaging.RawMessage(raw)
|
||||
msg.send_direct()
|
||||
sys.exit(0)
|
||||
else:
|
||||
msg = messaging.TextMessage(aprs_login, tocallsign, command)
|
||||
msg.send_direct()
|
||||
|
||||
if no_ack:
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
aprs_client = client.factory.create().client
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
except aprslib.exceptions.ConnectionDrop:
|
||||
LOG.error("Connection dropped, reconnecting")
|
||||
time.sleep(5)
|
||||
# Force the deletion of the client object connected to aprs
|
||||
# This will cause a reconnect, next time client.get_client()
|
||||
# is called
|
||||
aprs_client.reset()
|
||||
|
||||
|
||||
# main() ###
|
||||
@main.command()
|
||||
@click.option(
|
||||
"--loglevel",
|
||||
default="INFO",
|
||||
show_default=True,
|
||||
type=click.Choice(
|
||||
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
show_choices=True,
|
||||
help="The log level to use for aprsd.log",
|
||||
)
|
||||
@click.option("--quiet", is_flag=True, default=False, help="Don't log to stdout")
|
||||
@click.option(
|
||||
"-c",
|
||||
"--config",
|
||||
"config_file",
|
||||
show_default=True,
|
||||
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||
help="The aprsd config file to use for options.",
|
||||
)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--flush",
|
||||
"flush",
|
||||
is_flag=True,
|
||||
show_default=True,
|
||||
default=False,
|
||||
help="Flush out all old aged messages on disk.",
|
||||
)
|
||||
def server(
|
||||
loglevel,
|
||||
quiet,
|
||||
config_file,
|
||||
flush,
|
||||
):
|
||||
"""Start the aprsd server process."""
|
||||
global flask_enabled
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
if not quiet:
|
||||
click.echo("Load config")
|
||||
|
||||
config = aprsd_config.parse_config(config_file)
|
||||
|
||||
setup_logging(config, loglevel, quiet)
|
||||
level, msg = utils._check_version()
|
||||
if level:
|
||||
LOG.warning(msg)
|
||||
else:
|
||||
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]}")
|
||||
|
||||
if config["aprsd"].get("trace", False):
|
||||
trace.setup_tracing(["method", "api"])
|
||||
stats.APRSDStats(config)
|
||||
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
client.ClientFactory.setup(config)
|
||||
# Make sure we have 1 client transport enabled
|
||||
if not client.factory.is_client_enabled():
|
||||
LOG.error("No Clients are enabled in config.")
|
||||
sys.exit(-1)
|
||||
|
||||
# Creates the client object
|
||||
LOG.info("Creating client connection")
|
||||
client.factory.create().client
|
||||
|
||||
# Now load the msgTrack from disk if any
|
||||
packets.PacketList(config=config)
|
||||
if flush:
|
||||
LOG.debug("Deleting saved MsgTrack.")
|
||||
messaging.MsgTrack(config=config).flush()
|
||||
packets.WatchList(config=config)
|
||||
packets.SeenList(config=config)
|
||||
else:
|
||||
# Try and load saved MsgTrack list
|
||||
LOG.debug("Loading saved MsgTrack object.")
|
||||
messaging.MsgTrack(config=config).load()
|
||||
packets.WatchList(config=config).load()
|
||||
packets.SeenList(config=config).load()
|
||||
|
||||
# Create the initial PM singleton and Register plugins
|
||||
LOG.info("Loading Plugin Manager and registering plugins")
|
||||
plugin_manager = plugin.PluginManager(config)
|
||||
plugin_manager.setup_plugins()
|
||||
|
||||
rx_thread = threads.APRSDRXThread(
|
||||
msg_queues=threads.msg_queues,
|
||||
config=config,
|
||||
)
|
||||
rx_thread.start()
|
||||
|
||||
messaging.MsgTrack().restart()
|
||||
|
||||
keepalive = threads.KeepAliveThread(config=config)
|
||||
keepalive.start()
|
||||
|
||||
web_enabled = config.get("aprsd.web.enabled", default=False)
|
||||
|
||||
if web_enabled:
|
||||
flask_enabled = True
|
||||
(socketio, app) = flask.init_flask(config, loglevel, quiet)
|
||||
socketio.run(
|
||||
app,
|
||||
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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+29
-28
@@ -190,6 +190,7 @@ class Message(metaclass=abc.ABCMeta):
|
||||
fromcall,
|
||||
tocall,
|
||||
msg_id=None,
|
||||
allow_delay=True,
|
||||
):
|
||||
self.fromcall = fromcall
|
||||
self.tocall = tocall
|
||||
@@ -199,6 +200,10 @@ class Message(metaclass=abc.ABCMeta):
|
||||
msg_id = c.value
|
||||
self.id = msg_id
|
||||
|
||||
# do we try and save this message for later if we don't get
|
||||
# an ack? Some messages we don't want to do this ever.
|
||||
self.allow_delay = allow_delay
|
||||
|
||||
@abc.abstractmethod
|
||||
def send(self):
|
||||
"""Child class must declare."""
|
||||
@@ -214,8 +219,8 @@ class RawMessage(Message):
|
||||
|
||||
message = None
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__(None, None, msg_id=None)
|
||||
def __init__(self, message, allow_delay=True):
|
||||
super().__init__(fromcall=None, tocall=None, msg_id=None, allow_delay=allow_delay)
|
||||
self.message = message
|
||||
|
||||
def dict(self):
|
||||
@@ -269,11 +274,11 @@ class TextMessage(Message):
|
||||
msg_id=None,
|
||||
allow_delay=True,
|
||||
):
|
||||
super().__init__(fromcall, tocall, msg_id)
|
||||
super().__init__(
|
||||
fromcall=fromcall, tocall=tocall,
|
||||
msg_id=msg_id, allow_delay=allow_delay,
|
||||
)
|
||||
self.message = message
|
||||
# do we try and save this message for later if we don't get
|
||||
# an ack? Some messages we don't want to do this ever.
|
||||
self.allow_delay = allow_delay
|
||||
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
@@ -369,6 +374,8 @@ class SendMessageThread(threads.APRSDThread):
|
||||
# we reached the send limit, don't send again
|
||||
# TODO(hemna) - Need to put this in a delayed queue?
|
||||
LOG.info("Message Send Complete. Max attempts reached.")
|
||||
if not msg.allow_delay:
|
||||
tracker.remove(msg.id)
|
||||
return False
|
||||
|
||||
# Message is still outstanding and needs to be acked.
|
||||
@@ -527,6 +534,7 @@ def log_packet(packet):
|
||||
def log_message(
|
||||
header, raw, message, tocall=None, fromcall=None, msg_num=None,
|
||||
retry_number=None, ack=None, packet_type=None, uuid=None,
|
||||
console=None,
|
||||
):
|
||||
"""
|
||||
|
||||
@@ -542,37 +550,30 @@ def log_message(
|
||||
|
||||
log_list = [""]
|
||||
if retry_number:
|
||||
# LOG.info(" {} _______________(TX:{})".format(header, retry_number))
|
||||
log_list.append(f" {header} _______________(TX:{retry_number})")
|
||||
log_list.append(f"{header} _______________(TX:{retry_number})")
|
||||
else:
|
||||
# LOG.info(" {} _______________".format(header))
|
||||
log_list.append(f" {header} _______________")
|
||||
log_list.append(f"{header} _______________")
|
||||
|
||||
# LOG.info(" Raw : {}".format(raw))
|
||||
log_list.append(f" Raw : {raw}")
|
||||
log_list.append(f" Raw : {raw}")
|
||||
|
||||
if packet_type:
|
||||
# LOG.info(" Packet : {}".format(packet_type))
|
||||
log_list.append(f" Packet : {packet_type}")
|
||||
log_list.append(f" Packet : {packet_type}")
|
||||
if tocall:
|
||||
# LOG.info(" To : {}".format(tocall))
|
||||
log_list.append(f" To : {tocall}")
|
||||
log_list.append(f" To : {tocall}")
|
||||
if fromcall:
|
||||
# LOG.info(" From : {}".format(fromcall))
|
||||
log_list.append(f" From : {fromcall}")
|
||||
log_list.append(f" From : {fromcall}")
|
||||
|
||||
if ack:
|
||||
# LOG.info(" Ack : {}".format(ack))
|
||||
log_list.append(f" Ack : {ack}")
|
||||
log_list.append(f" Ack : {ack}")
|
||||
else:
|
||||
# LOG.info(" Message : {}".format(message))
|
||||
log_list.append(f" Message : {message}")
|
||||
log_list.append(f" Message : {message}")
|
||||
if msg_num:
|
||||
# LOG.info(" Msg number : {}".format(msg_num))
|
||||
log_list.append(f" Msg number : {msg_num}")
|
||||
log_list.append(f" Msg # : {msg_num}")
|
||||
if uuid:
|
||||
log_list.append(f" UUID : {uuid}")
|
||||
# LOG.info(" {} _______________ Complete".format(header))
|
||||
log_list.append(f" {header} _______________ Complete")
|
||||
log_list.append(f" UUID : {uuid}")
|
||||
log_list.append(f"{header} _______________ Complete")
|
||||
|
||||
LOG.info("\n".join(log_list))
|
||||
if console:
|
||||
console.log("\n".join(log_list))
|
||||
else:
|
||||
LOG.info("\n".join(log_list))
|
||||
|
||||
@@ -92,7 +92,7 @@ class ObjectStoreMixin:
|
||||
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
|
||||
)
|
||||
LOG.debug(f"{self.data}")
|
||||
except pickle.UnpicklingError as ex:
|
||||
except (pickle.UnpicklingError, Exception) as ex:
|
||||
LOG.error(f"Failed to UnPickle {self._save_filename()}")
|
||||
LOG.error(ex)
|
||||
self.data = {}
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ class WatchList(objectstore.ObjectStoreMixin):
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
ring_size = config["aprsd"]["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", []):
|
||||
call = callsign.replace("*", "")
|
||||
|
||||
+18
-5
@@ -12,6 +12,7 @@ import threading
|
||||
import pluggy
|
||||
from thesmuggler import smuggle
|
||||
|
||||
import aprsd
|
||||
from aprsd import client, messaging, packets, threads
|
||||
|
||||
|
||||
@@ -24,7 +25,6 @@ CORE_MESSAGE_PLUGINS = [
|
||||
"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",
|
||||
@@ -52,7 +52,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||
config = None
|
||||
rx_count = 0
|
||||
tx_count = 0
|
||||
version = "1.0"
|
||||
version = aprsd.__version__
|
||||
|
||||
# Holds the list of APRSDThreads that the plugin creates
|
||||
threads = []
|
||||
@@ -177,7 +177,6 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
)
|
||||
if result:
|
||||
self.tx_inc()
|
||||
wl.update_seen(packet)
|
||||
else:
|
||||
LOG.warning(f"{self.__class__} plugin is not enabled")
|
||||
|
||||
@@ -242,11 +241,24 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
if result:
|
||||
self.tx_inc()
|
||||
else:
|
||||
LOG.warning(f"{self.__class__} isn't enabled.")
|
||||
result = f"{self.__class__.__name__} isn't enabled"
|
||||
LOG.warning(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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}")
|
||||
self.enabled = False
|
||||
|
||||
|
||||
class HelpPlugin(APRSDRegexCommandPluginBase):
|
||||
"""Help Plugin that is always enabled.
|
||||
|
||||
@@ -425,7 +437,8 @@ class PluginManager:
|
||||
)
|
||||
self._pluggy_pm.register(plugin_obj)
|
||||
except Exception as ex:
|
||||
LOG.exception(f"Couldn't load plugin '{plugin_name}'", ex)
|
||||
LOG.error(f"Couldn't load plugin '{plugin_name}'")
|
||||
LOG.exception(ex)
|
||||
|
||||
def reload_plugins(self):
|
||||
with self.lock:
|
||||
|
||||
+40
-47
@@ -59,9 +59,9 @@ class EmailInfo:
|
||||
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Email Plugin."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^-.*"
|
||||
command_name = "email"
|
||||
short_description = "Send and Receive email"
|
||||
|
||||
# message_number:time combos so we don't resend the same email in
|
||||
# five mins {int:int}
|
||||
@@ -193,8 +193,8 @@ def _imap_connect(config):
|
||||
ssl=use_ssl,
|
||||
timeout=30,
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error("Failed to connect IMAP server", e)
|
||||
except Exception:
|
||||
LOG.exception("Failed to connect IMAP server")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -348,8 +348,8 @@ def parse_email(msgid, data, server):
|
||||
LOG.debug(f"Got a message from '{from_addr}'")
|
||||
try:
|
||||
m = server.fetch([msgid], ["RFC822"])
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't fetch email from server in parse_email", e)
|
||||
except Exception:
|
||||
LOG.exception("Couldn't fetch email from server in parse_email")
|
||||
return
|
||||
|
||||
msg = email.message_from_string(m[msgid][b"RFC822"].decode(errors="ignore"))
|
||||
@@ -414,9 +414,9 @@ def parse_email(msgid, data, server):
|
||||
# it below, also with errors='ignore'
|
||||
try:
|
||||
body = body.decode(errors="ignore")
|
||||
except Exception as e:
|
||||
LOG.error("Unicode decode failure: " + str(e))
|
||||
LOG.error("Unidoce decode failed: " + str(body))
|
||||
except Exception:
|
||||
LOG.exception("Unicode decode failure")
|
||||
LOG.error(f"Unidoce decode failed: {str(body)}")
|
||||
body = "Unreadable unicode msg"
|
||||
# strip all html tags
|
||||
body = re.sub("<[^<]+?>", "", body)
|
||||
@@ -437,13 +437,13 @@ def send_email(config, to_addr, content):
|
||||
LOG.info("Sending Email_________________")
|
||||
|
||||
if to_addr in shortcuts:
|
||||
LOG.info("To : " + to_addr)
|
||||
LOG.info(f"To : {to_addr}")
|
||||
to_addr = email_address
|
||||
LOG.info(" (" + to_addr + ")")
|
||||
LOG.info(f" ({to_addr})")
|
||||
subject = config["ham"]["callsign"]
|
||||
# content = content + "\n\n(NOTE: reply with one line)"
|
||||
LOG.info("Subject : " + subject)
|
||||
LOG.info("Body : " + content)
|
||||
LOG.info(f"Subject : {subject}")
|
||||
LOG.info(f"Body : {content}")
|
||||
|
||||
# check email more often since there's activity right now
|
||||
EmailInfo().delay = 60
|
||||
@@ -461,9 +461,8 @@ def send_email(config, to_addr, content):
|
||||
msg.as_string(),
|
||||
)
|
||||
stats.APRSDStats().email_tx_inc()
|
||||
except Exception as e:
|
||||
msg = getattr(e, "message", repr(e))
|
||||
LOG.error("Sendmail Error!!!! '{}'", msg)
|
||||
except Exception:
|
||||
LOG.exception("Sendmail Error!!!!")
|
||||
server.quit()
|
||||
return -1
|
||||
server.quit()
|
||||
@@ -484,14 +483,14 @@ def resend_email(config, count, fromcall):
|
||||
|
||||
try:
|
||||
server = _imap_connect(config)
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
|
||||
except Exception:
|
||||
LOG.exception("Failed to Connect to IMAP. Cannot resend email ")
|
||||
return
|
||||
|
||||
try:
|
||||
messages = server.search(["SINCE", today])
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't search for emails in resend_email ", e)
|
||||
except Exception:
|
||||
LOG.exception("Couldn't search for emails in resend_email ")
|
||||
return
|
||||
|
||||
# LOG.debug("%d messages received today" % len(messages))
|
||||
@@ -503,8 +502,8 @@ def resend_email(config, count, fromcall):
|
||||
for message in messages:
|
||||
try:
|
||||
parts = server.fetch(message, ["ENVELOPE"]).items()
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't fetch email parts in resend_email", e)
|
||||
except Exception:
|
||||
LOG.exception("Couldn't fetch email parts in resend_email")
|
||||
continue
|
||||
|
||||
for msgid, data in list(parts):
|
||||
@@ -513,8 +512,8 @@ def resend_email(config, count, fromcall):
|
||||
# unset seen flag, will stay bold in email client
|
||||
try:
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to remove SEEN flag in resend_email", e)
|
||||
except Exception:
|
||||
LOG.exception("Failed to remove SEEN flag in resend_email")
|
||||
|
||||
if from_addr in shortcuts_inverted:
|
||||
# reverse lookup of a shortcut
|
||||
@@ -592,24 +591,21 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
|
||||
try:
|
||||
server = _imap_connect(self.config)
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to connect.", e)
|
||||
except Exception:
|
||||
LOG.exception("IMAP Failed to connect")
|
||||
return True
|
||||
|
||||
try:
|
||||
messages = server.search(["SINCE", today])
|
||||
except Exception as e:
|
||||
LOG.exception(
|
||||
"IMAP failed to search for messages since today.",
|
||||
e,
|
||||
)
|
||||
except Exception:
|
||||
LOG.exception("IMAP failed to search for messages since today.")
|
||||
return True
|
||||
LOG.debug(f"{len(messages)} messages received today")
|
||||
|
||||
try:
|
||||
_msgs = server.fetch(messages, ["ENVELOPE"])
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to fetch/flag messages: ", e)
|
||||
except Exception:
|
||||
LOG.exception("IMAP failed to fetch/flag messages: ")
|
||||
return True
|
||||
|
||||
for msgid, data in _msgs.items():
|
||||
@@ -637,27 +633,24 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
x.decode(errors="ignore")
|
||||
for x in server.get_flags(msgid)[msgid]
|
||||
]
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to get flags.", e)
|
||||
except Exception:
|
||||
LOG.error("Failed to get flags.")
|
||||
break
|
||||
|
||||
if "APRS" not in taglist:
|
||||
# if msg not flagged as sent via aprs
|
||||
try:
|
||||
server.fetch([msgid], ["RFC822"])
|
||||
except Exception as e:
|
||||
LOG.exception(
|
||||
"Failed single server fetch for RFC822",
|
||||
e,
|
||||
)
|
||||
except Exception:
|
||||
LOG.exception("Failed single server fetch for RFC822")
|
||||
break
|
||||
|
||||
(body, from_addr) = parse_email(msgid, data, server)
|
||||
# unset seen flag, will stay bold in email client
|
||||
try:
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to remove flags SEEN", e)
|
||||
except Exception:
|
||||
LOG.exception("Failed to remove flags SEEN")
|
||||
# Not much we can do here, so lets try and
|
||||
# send the aprs message anyway
|
||||
|
||||
@@ -676,13 +669,13 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
try:
|
||||
server.add_flags(msgid, ["APRS"])
|
||||
# unset seen flag, will stay bold in email client
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't add APRS flag to email", e)
|
||||
except Exception:
|
||||
LOG.exception("Couldn't add APRS flag to email")
|
||||
|
||||
try:
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't remove seen flag from email", e)
|
||||
except Exception:
|
||||
LOG.exception("Couldn't remove seen flag from email")
|
||||
|
||||
# check email more often since we just received an email
|
||||
EmailInfo().delay = 60
|
||||
@@ -692,8 +685,8 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
self.past = datetime.datetime.now()
|
||||
try:
|
||||
server.logout()
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to logout: ", e)
|
||||
except Exception:
|
||||
LOG.exception("IMAP failed to logout: ")
|
||||
return True
|
||||
else:
|
||||
# We haven't hit the email delay yet.
|
||||
|
||||
@@ -11,9 +11,18 @@ LOG = logging.getLogger("APRSD")
|
||||
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Fortune."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[fF]"
|
||||
command_name = "fortune"
|
||||
short_description = "Give me a fortune"
|
||||
|
||||
fortune_path = None
|
||||
|
||||
def setup(self):
|
||||
self.fortune_path = shutil.which("fortune")
|
||||
if not self.fortune_path:
|
||||
self.enabled = False
|
||||
else:
|
||||
self.enabled = True
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -25,13 +34,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
reply = None
|
||||
|
||||
fortune_path = shutil.which("fortune")
|
||||
if not fortune_path:
|
||||
reply = "Fortune command not installed"
|
||||
return reply
|
||||
|
||||
try:
|
||||
cmnd = [fortune_path, "-s", "-n 60"]
|
||||
cmnd = [self.fortune_path, "-s", "-n 60"]
|
||||
command = " ".join(cmnd)
|
||||
output = subprocess.check_output(
|
||||
command,
|
||||
|
||||
@@ -8,12 +8,15 @@ from aprsd import plugin, plugin_utils, trace
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||
"""Location!"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[lL]"
|
||||
command_name = "location"
|
||||
short_description = "Where in the world is a CALLSIGN's last GPS beacon?"
|
||||
|
||||
def setup(self):
|
||||
self.ensure_aprs_fi_key()
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -22,13 +25,6 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
try:
|
||||
self.config.check_option(["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"]
|
||||
|
||||
# optional second argument is a callsign to search
|
||||
|
||||
+16
-4
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from aprsd import packets, plugin
|
||||
from aprsd import messaging, packets, plugin, trace
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@@ -15,8 +15,9 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
||||
seen was older than the configured age limit.
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
short_description = "Notify me when a CALLSIGN is recently seen on APRS-IS"
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
LOG.info("NotifySeenPlugin")
|
||||
|
||||
@@ -37,12 +38,23 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
||||
packet_type = packets.get_packet_type(packet)
|
||||
# we shouldn't notify the alert user that they are online.
|
||||
if fromcall != notify_callsign:
|
||||
return f"{fromcall} was just seen by type:'{packet_type}'"
|
||||
msg = messaging.TextMessage(
|
||||
self.config["aprs"]["login"],
|
||||
notify_callsign,
|
||||
f"{fromcall} was just seen by type:'{packet_type}'",
|
||||
# We don't need to keep this around if it doesn't go thru
|
||||
allow_delay=False,
|
||||
)
|
||||
return msg
|
||||
else:
|
||||
LOG.debug("fromcall and notify_callsign are the same, not notifying")
|
||||
return messaging.NULL_MESSAGE
|
||||
else:
|
||||
LOG.debug(
|
||||
"Not old enough to notify callsign '{}' : {} < {}".format(
|
||||
"Not old enough to notify on callsign '{}' : {} < {}".format(
|
||||
fromcall,
|
||||
age,
|
||||
wl.max_delta(),
|
||||
),
|
||||
)
|
||||
return messaging.NULL_MESSAGE
|
||||
|
||||
@@ -10,9 +10,9 @@ LOG = logging.getLogger("APRSD")
|
||||
class PingPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Ping."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[pP]"
|
||||
command_name = "ping"
|
||||
short_description = "reply with a Pong!"
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
|
||||
@@ -11,9 +11,9 @@ LOG = logging.getLogger("APRSD")
|
||||
class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Query command."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = r"^\!.*"
|
||||
command_name = "query"
|
||||
short_description = "APRSD Owner command to query messages in the MsgTrack"
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
import yfinance as yf
|
||||
|
||||
from aprsd import plugin, trace
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class StockPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Stock market plugin for fetching stock quotes"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[sS]"
|
||||
command_name = "stock"
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
LOG.info("StockPlugin")
|
||||
|
||||
# fromcall = packet.get("from")
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
stock_symbol = searchcall.upper()
|
||||
else:
|
||||
reply = "No stock symbol"
|
||||
return reply
|
||||
|
||||
LOG.info(f"Fetch stock quote for '{stock_symbol}'")
|
||||
|
||||
try:
|
||||
stock = yf.Ticker(stock_symbol)
|
||||
reply = "{} - ask: {} high: {} low: {}".format(
|
||||
stock_symbol,
|
||||
stock.info["ask"],
|
||||
stock.info["dayHigh"],
|
||||
stock.info["dayLow"],
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error(
|
||||
f"Failed to fetch stock '{stock_symbol}' from yahoo '{e}'",
|
||||
)
|
||||
reply = f"Failed to fetch stock '{stock_symbol}'"
|
||||
|
||||
return reply.rstrip()
|
||||
+11
-19
@@ -14,9 +14,9 @@ LOG = logging.getLogger("APRSD")
|
||||
class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Time command."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[tT]"
|
||||
command_name = "time"
|
||||
short_description = "What is the current local time."
|
||||
|
||||
def _get_local_tz(self):
|
||||
return pytz.timezone(time.strftime("%Z"))
|
||||
@@ -49,12 +49,15 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
return self.build_date_str(localzone)
|
||||
|
||||
|
||||
class TimeOpenCageDataPlugin(TimePlugin):
|
||||
class TimeOpenCageDataPlugin(TimePlugin, plugin.APRSFIKEYMixin):
|
||||
"""geocage based timezone fetching."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[tT]"
|
||||
command_name = "time"
|
||||
short_description = "Current time of GPS beacon timezone. Uses OpenCage"
|
||||
|
||||
def setup(self):
|
||||
self.ensure_aprs_fi_key()
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -62,13 +65,6 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
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"]
|
||||
|
||||
# optional second argument is a callsign to search
|
||||
@@ -115,12 +111,15 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
||||
return self.build_date_str(localzone)
|
||||
|
||||
|
||||
class TimeOWMPlugin(TimePlugin):
|
||||
class TimeOWMPlugin(TimePlugin, plugin.APRSFIKEYMixin):
|
||||
"""OpenWeatherMap based timezone fetching."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[tT]"
|
||||
command_name = "time"
|
||||
short_description = "Current time of GPS beacon's timezone. Uses OpenWeatherMap"
|
||||
|
||||
def setup(self):
|
||||
self.ensure_aprs_fi_key()
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -128,13 +127,6 @@ class TimeOWMPlugin(TimePlugin):
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
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"
|
||||
|
||||
# optional second argument is a callsign to search
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
|
||||
@@ -10,9 +10,9 @@ LOG = logging.getLogger("APRSD")
|
||||
class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Version of APRSD Plugin."""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[vV]"
|
||||
command_name = "version"
|
||||
short_description = "What is the APRSD Version"
|
||||
|
||||
# message_number:time combos so we don't resend the same email in
|
||||
# five mins {int:int}
|
||||
|
||||
+20
-16
@@ -10,7 +10,7 @@ from aprsd import plugin, plugin_utils, trace
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||
"""USWeather Command
|
||||
|
||||
Returns a weather report for the calling weather station
|
||||
@@ -23,9 +23,12 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"weather" - returns weather near the calling callsign
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[wW]"
|
||||
command_name = "USWeather"
|
||||
short_description = "Provide USA only weather of GPS Beacon location"
|
||||
|
||||
def setup(self):
|
||||
self.ensure_aprs_fi_key()
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -33,20 +36,18 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
fromcall = packet.get("from")
|
||||
# message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
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"]
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
||||
return "Failed to fetch location"
|
||||
return "Failed to fetch aprs.fi location"
|
||||
|
||||
LOG.debug(f"LocationPlugin: aprs_data = {aprs_data}")
|
||||
if not len(aprs_data["entries"]):
|
||||
LOG.error("Didn't get any entries from aprs.fi")
|
||||
return "Failed to fetch aprs.fi location"
|
||||
|
||||
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
|
||||
@@ -71,7 +72,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
return reply
|
||||
|
||||
|
||||
class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||
"""METAR Command
|
||||
|
||||
This provides a METAR weather report from a station near the caller
|
||||
@@ -86,9 +87,12 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[metar]"
|
||||
command_name = "USMetar"
|
||||
short_description = "USA only METAR of GPS Beacon location"
|
||||
|
||||
def setup(self):
|
||||
self.ensure_aprs_fi_key()
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
@@ -126,12 +130,12 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
||||
return "Failed to fetch location"
|
||||
return "Failed to fetch aprs.fi location"
|
||||
|
||||
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
|
||||
if not len(aprs_data["entries"]):
|
||||
LOG.error("Found no entries from aprs.fi!")
|
||||
return "Failed to fetch location"
|
||||
return "Failed to fetch aprs.fi location"
|
||||
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
@@ -178,9 +182,9 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[wW]"
|
||||
command_name = "OpenWeatherMap"
|
||||
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
||||
|
||||
def help(self):
|
||||
_help = [
|
||||
@@ -308,9 +312,9 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
docker build -f Dockerfile -t avwx-api:master .
|
||||
"""
|
||||
|
||||
version = "1.0"
|
||||
command_regex = "^[mM]"
|
||||
command_name = "AVWXWeather"
|
||||
short_description = "AVWX weather of GPS Beacon location"
|
||||
|
||||
def help(self):
|
||||
_help = [
|
||||
|
||||
+7
-5
@@ -17,7 +17,6 @@ RX_THREAD = "RX"
|
||||
EMAIL_THREAD = "Email"
|
||||
|
||||
rx_msg_queue = queue.Queue(maxsize=20)
|
||||
logging_queue = queue.Queue()
|
||||
msg_queues = {
|
||||
"rx": rx_msg_queue,
|
||||
}
|
||||
@@ -89,7 +88,7 @@ class KeepAliveThread(APRSDThread):
|
||||
tracemalloc.start()
|
||||
super().__init__("KeepAlive")
|
||||
self.config = config
|
||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
|
||||
self.max_delta = datetime.timedelta(**max_timeout)
|
||||
|
||||
def loop(self):
|
||||
@@ -203,13 +202,13 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
self.packet = packet
|
||||
self.config = config
|
||||
name = self.packet["raw"][:10]
|
||||
super().__init__(f"RX_PACKET-{name}")
|
||||
super().__init__(f"RXPKT-{name}")
|
||||
|
||||
def process_ack_packet(self, packet):
|
||||
ack_num = packet.get("msgNo")
|
||||
LOG.info(f"Got ack for message {ack_num}")
|
||||
messaging.log_message(
|
||||
"ACK",
|
||||
"RXACK",
|
||||
packet["raw"],
|
||||
None,
|
||||
ack=ack_num,
|
||||
@@ -262,6 +261,8 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
pm = plugin.PluginManager()
|
||||
try:
|
||||
results = pm.run(packet)
|
||||
wl = packets.WatchList()
|
||||
wl.update_seen(packet)
|
||||
replied = False
|
||||
for reply in results:
|
||||
if isinstance(reply, list):
|
||||
@@ -310,7 +311,8 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||
)
|
||||
msg.send()
|
||||
except Exception as ex:
|
||||
LOG.exception("Plugin failed!!!", ex)
|
||||
LOG.error("Plugin failed!!!")
|
||||
LOG.exception(ex)
|
||||
# Do we need to send a reply?
|
||||
if tocall == self.config["aprs"]["login"]:
|
||||
reply = "A Plugin failed! try again?"
|
||||
|
||||
@@ -60,6 +60,17 @@ def end_substr(original, substr):
|
||||
return idx
|
||||
|
||||
|
||||
def rgb_from_name(name):
|
||||
"""Create an rgb tuple from a string."""
|
||||
hash = 0
|
||||
for char in name:
|
||||
hash = ord(char) + ((hash << 5) - hash)
|
||||
red = hash & 255
|
||||
green = (hash >> 8) & 255
|
||||
blue = (hash >> 16) & 255
|
||||
return red, green, blue
|
||||
|
||||
|
||||
def human_size(bytes, units=None):
|
||||
"""Returns a human readable string representation of bytes"""
|
||||
if not units:
|
||||
|
||||
+9
-10
@@ -1,4 +1,5 @@
|
||||
FROM python:3-bullseye as aprsd
|
||||
#FROM python:3-bullseye as aprsd
|
||||
FROM ubuntu:focal as aprsd
|
||||
|
||||
# Dockerfile for building a container during aprsd development.
|
||||
|
||||
@@ -13,22 +14,17 @@ ENV GID=${GID:-1000}
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt update
|
||||
RUN apt install -y git build-essential
|
||||
RUN apt install -y libffi-dev python3-dev libssl-dev
|
||||
#RUN apt-get install -y apt-utils
|
||||
#RUN apt-get install -y pkg-config
|
||||
#RUN apt upgrade -y
|
||||
#RUN apk add --update git wget bash
|
||||
#RUN apk add --update gcc linux-headers musl-dev libffi-dev libc-dev
|
||||
#RUN apk add --update openssl-dev
|
||||
#RUN add cmd:pip3 lsb-release
|
||||
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev
|
||||
RUN apt install -y python3 python3-pip python3-dev python3-lxml
|
||||
|
||||
RUN addgroup --gid $GID $APRS_USER
|
||||
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
||||
|
||||
# Install aprsd
|
||||
RUN /usr/local/bin/pip3 install aprsd==2.3.1
|
||||
RUN pip install aprsd
|
||||
|
||||
# Ensure /config is there with a default config file
|
||||
USER root
|
||||
@@ -43,3 +39,6 @@ VOLUME ["/config", "/plugins"]
|
||||
USER $APRS_USER
|
||||
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
|
||||
|
||||
+18
-7
@@ -1,4 +1,5 @@
|
||||
FROM python:3.8-slim as aprsd
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM ubuntu:focal as aprsd
|
||||
|
||||
# Dockerfile for building a container during aprsd development.
|
||||
ARG branch
|
||||
@@ -13,11 +14,14 @@ ENV VIRTUAL_ENV=$HOME/.venv3
|
||||
ENV UID=${UID:-1000}
|
||||
ENV GID=${GID:-1000}
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV INSTALL=$HOME/install
|
||||
RUN apt update
|
||||
RUN apt install -y git build-essential
|
||||
RUN apt install -y libffi-dev python3-dev libssl-dev
|
||||
RUN apt install -y bash fortune
|
||||
RUN apt install -y --no-install-recommends git build-essential bash fortune
|
||||
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev
|
||||
RUN apt install -y python3 python3-pip python3-dev python3-lxml
|
||||
#RUN apt-get clean
|
||||
RUN apt-get -o Dpkg::Options::="--force-confmiss" install --reinstall netbase
|
||||
|
||||
RUN addgroup --gid 1001 $APRS_USER
|
||||
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
||||
@@ -25,17 +29,21 @@ RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
WORKDIR $HOME
|
||||
USER $APRS_USER
|
||||
RUN pip3 install wheel
|
||||
RUN pip install wheel
|
||||
#RUN python3 -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
#ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
RUN echo "export PATH=\$PATH:\$HOME/.local/bin" >> $HOME/.bashrc
|
||||
RUN cat $HOME/.bashrc
|
||||
|
||||
USER root
|
||||
WORKDIR $HOME
|
||||
RUN mkdir $INSTALL
|
||||
RUN git clone -b $APRSD_BRANCH $APRSD $INSTALL/aprsd
|
||||
RUN cd $INSTALL/aprsd && pip3 install .
|
||||
RUN cd $INSTALL/aprsd && pip3 install -v .
|
||||
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
|
||||
@@ -48,3 +56,6 @@ VOLUME ["/config", "/plugins"]
|
||||
|
||||
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
|
||||
|
||||
+9
-1
@@ -13,6 +13,12 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
|
||||
LOG_LEVEL="DEBUG"
|
||||
fi
|
||||
|
||||
echo "Log level is set to ${LOG_LEVEL}";
|
||||
|
||||
# check to see if there is a config file
|
||||
APRSD_CONFIG="/config/aprsd.yml"
|
||||
if [ ! -e "$APRSD_CONFIG" ]; then
|
||||
@@ -20,4 +26,6 @@ if [ ! -e "$APRSD_CONFIG" ]; then
|
||||
aprsd sample-config > $APRSD_CONFIG
|
||||
fi
|
||||
|
||||
/usr/local/bin/aprsd server -c $APRSD_CONFIG --loglevel DEBUG
|
||||
export COLUMNS=200
|
||||
python3 -m rich.diagnose
|
||||
exec aprsd server -c $APRSD_CONFIG --loglevel ${LOG_LEVEL}
|
||||
|
||||
+13
-3
@@ -1,6 +1,10 @@
|
||||
#!/bin/bash
|
||||
# Official docker image build script.
|
||||
|
||||
# docker buildx create --name multiarch \
|
||||
# --platform linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64 \
|
||||
# --config ./buildkit.toml --use --driver-opt image=moby/buildkit:master
|
||||
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
@@ -40,11 +44,12 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
VERSION="2.3.1"
|
||||
VERSION="2.5.8"
|
||||
|
||||
if [ $ALL_PLATFORMS -eq 1 ]
|
||||
then
|
||||
PLATFORMS="linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64"
|
||||
PLATFORMS="linux/arm/v7,linux/arm64,linux/amd64"
|
||||
#PLATFORMS="linux/arm/v7,linux/arm/v6,linux/amd64"
|
||||
else
|
||||
PLATFORMS="linux/amd64"
|
||||
fi
|
||||
@@ -54,8 +59,10 @@ echo "Build with tag=${TAG} BRANCH=${BRANCH} dev?=${DEV} platforms?=${PLATFORMS}
|
||||
|
||||
echo "Destroying old multiarch build container"
|
||||
docker buildx rm multiarch
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
echo "Creating new buildx container"
|
||||
docker buildx create --name multiarch --platform linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64 --config ./buildkit.toml --use --driver-opt image=moby/buildkit:master
|
||||
docker buildx create --name multiarch --driver docker-container --use --platform linux/arm/v7,linux/arm/v6,linux/arm64,linux/amd64 --config ./buildkit.toml --use --driver-opt image=moby/buildkit:master
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
if [ $DEV -eq 1 ]
|
||||
then
|
||||
@@ -68,9 +75,12 @@ else
|
||||
# Use this script to locally build the docker image
|
||||
echo "Build with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
|
||||
docker buildx build --push --platform $PLATFORMS \
|
||||
--allow security.insecure \
|
||||
-t hemna6969/aprsd:$VERSION \
|
||||
-t hemna6969/aprsd:$TAG \
|
||||
-t hemna6969/aprsd:latest \
|
||||
-t harbor.hemna.com/hemna6969/aprsd:$TAG \
|
||||
-t harbor.hemna.com/hemna6969/aprsd:$VERSION \
|
||||
-t harbor.hemna.com/hemna6969/aprsd:latest \
|
||||
-f Dockerfile .
|
||||
fi
|
||||
|
||||
@@ -9,3 +9,4 @@ services:
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- APRS_PLUGINS=aprsd-slack-plugin>=1.0.2
|
||||
- LOG_LEVEL=ERROR
|
||||
|
||||
+5
-2
@@ -1,5 +1,5 @@
|
||||
aioax25>=0.0.10
|
||||
aprslib
|
||||
aprslib>=0.7.0
|
||||
click
|
||||
click-completion
|
||||
flask
|
||||
@@ -17,7 +17,10 @@ pytz
|
||||
requests
|
||||
six
|
||||
thesmuggler
|
||||
yfinance
|
||||
update_checker
|
||||
flask-socketio
|
||||
eventlet
|
||||
tabulate
|
||||
rich
|
||||
# For the list-plugins pypi.org search scraping
|
||||
beautifulsoup4
|
||||
|
||||
+16
-20
@@ -6,10 +6,12 @@
|
||||
#
|
||||
aioax25==0.0.10
|
||||
# via -r requirements.in
|
||||
aprslib==0.6.47
|
||||
aprslib==0.7.0
|
||||
# via -r requirements.in
|
||||
backoff==1.11.1
|
||||
# via opencage
|
||||
beautifulsoup4==4.10.0
|
||||
# via -r requirements.in
|
||||
bidict==0.21.2
|
||||
# via python-socketio
|
||||
certifi==2021.5.30
|
||||
@@ -25,6 +27,10 @@ click==8.0.1
|
||||
# flask
|
||||
click-completion==0.5.2
|
||||
# via -r requirements.in
|
||||
colorama==0.4.4
|
||||
# via rich
|
||||
commonmark==0.9.1
|
||||
# via rich
|
||||
contexter==0.1.4
|
||||
# via signalslot
|
||||
cryptography==3.4.7
|
||||
@@ -63,20 +69,10 @@ jinja2==3.0.1
|
||||
# via
|
||||
# click-completion
|
||||
# flask
|
||||
lxml==4.6.3
|
||||
# via yfinance
|
||||
markupsafe==2.0.1
|
||||
# via jinja2
|
||||
multitasking==0.0.9
|
||||
# via yfinance
|
||||
numpy==1.21.2
|
||||
# via
|
||||
# pandas
|
||||
# yfinance
|
||||
opencage==2.0.0
|
||||
# via -r requirements.in
|
||||
pandas==1.3.2
|
||||
# via yfinance
|
||||
pbr==5.6.0
|
||||
# via -r requirements.in
|
||||
pluggy==1.0.0
|
||||
@@ -85,20 +81,18 @@ py3-validate-email==1.0.1
|
||||
# via -r requirements.in
|
||||
pycparser==2.20
|
||||
# via cffi
|
||||
pygments==2.10.0
|
||||
# via rich
|
||||
pyopenssl==20.0.1
|
||||
# via opencage
|
||||
pyserial==3.5
|
||||
# via aioax25
|
||||
python-dateutil==2.8.1
|
||||
# via pandas
|
||||
python-engineio==4.2.1
|
||||
# via python-socketio
|
||||
python-socketio==5.4.0
|
||||
# via flask-socketio
|
||||
pytz==2021.1
|
||||
# via
|
||||
# -r requirements.in
|
||||
# pandas
|
||||
# via -r requirements.in
|
||||
pyyaml==5.4.1
|
||||
# via -r requirements.in
|
||||
requests==2.26.0
|
||||
@@ -106,7 +100,8 @@ requests==2.26.0
|
||||
# -r requirements.in
|
||||
# opencage
|
||||
# update-checker
|
||||
# yfinance
|
||||
rich==10.15.2
|
||||
# via -r requirements.in
|
||||
shellingham==1.4.0
|
||||
# via click-completion
|
||||
signalslot==0.1.2
|
||||
@@ -118,8 +113,11 @@ six==1.16.0
|
||||
# eventlet
|
||||
# imapclient
|
||||
# pyopenssl
|
||||
# python-dateutil
|
||||
# signalslot
|
||||
soupsieve==2.3.1
|
||||
# via beautifulsoup4
|
||||
tabulate==0.8.9
|
||||
# via -r requirements.in
|
||||
thesmuggler==1.0.1
|
||||
# via -r requirements.in
|
||||
update-checker==0.18.0
|
||||
@@ -130,5 +128,3 @@ weakrefmethod==1.0.3
|
||||
# via signalslot
|
||||
werkzeug==2.0.0
|
||||
# via flask
|
||||
yfinance==0.1.63
|
||||
# via -r requirements.in
|
||||
|
||||
@@ -33,11 +33,7 @@ packages =
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
aprsd = aprsd.main:main
|
||||
aprsd-listen = aprsd.listen:main
|
||||
aprsd-dev = aprsd.dev:main
|
||||
aprsd-healthcheck = aprsd.healthcheck:main
|
||||
fake_aprs = aprsd.fake_aprs:main
|
||||
aprsd = aprsd.aprsd:main
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = docs
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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": {}}
|
||||
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.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", "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.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
|
||||
@@ -0,0 +1,94 @@
|
||||
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 send_message # noqa
|
||||
|
||||
|
||||
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||
|
||||
|
||||
class TestSendMessageCommand(unittest.TestCase):
|
||||
|
||||
def _build_config(self, login=None, password=None):
|
||||
config = {"aprs": {}}
|
||||
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.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, ["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.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."""
|
||||
|
||||
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.log.setup_logging")
|
||||
def test_no_tocallsign(self, mock_logging, mock_parse_config):
|
||||
"""Make sure we get an error if there is no tocallsign."""
|
||||
|
||||
runner = CliRunner()
|
||||
mock_parse_config.return_value = self._build_config(
|
||||
login="something",
|
||||
password="another",
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
cli, ["send-message"],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
assert result.exit_code == 2
|
||||
assert "Error: Missing argument 'TOCALLSIGN'" in result.output
|
||||
|
||||
@mock.patch("aprsd.config.parse_config")
|
||||
@mock.patch("aprsd.log.setup_logging")
|
||||
def test_no_command(self, mock_logging, mock_parse_config):
|
||||
"""Make sure we get an error if there is no command."""
|
||||
|
||||
runner = CliRunner()
|
||||
mock_parse_config.return_value = self._build_config(
|
||||
login="something",
|
||||
password="another",
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
cli, ["send-message", "WB4BOR"],
|
||||
catch_exceptions=False,
|
||||
)
|
||||
assert result.exit_code == 2
|
||||
assert "Error: Missing argument 'COMMAND...'" in result.output
|
||||
+8
-2
@@ -2,7 +2,7 @@ from aprsd import packets, plugin, threads
|
||||
|
||||
|
||||
FAKE_MESSAGE_TEXT = "fake MeSSage"
|
||||
FAKE_FROM_CALLSIGN = "KFART"
|
||||
FAKE_FROM_CALLSIGN = "KFAKE"
|
||||
FAKE_TO_CALLSIGN = "KMINE"
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class FakeThread(threads.APRSDThread):
|
||||
super().__init__("FakeThread")
|
||||
|
||||
def loop(self):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FakeBaseThreadsPlugin(plugin.APRSDPluginBase):
|
||||
@@ -71,3 +71,9 @@ class FakeRegexCommandPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
def process(self, packet):
|
||||
return FAKE_MESSAGE_TEXT
|
||||
|
||||
|
||||
class FakeWatchListPlugin(plugin.APRSDWatchListPluginBase):
|
||||
|
||||
def process(self, packet):
|
||||
return FAKE_MESSAGE_TEXT
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from unittest import mock
|
||||
|
||||
from aprsd.plugins import fortune as fortune_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
|
||||
|
||||
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)
|
||||
expected = "FortunePlugin isn't enabled"
|
||||
packet = fake.fake_packet(message="fortune")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("subprocess.check_output")
|
||||
@mock.patch("shutil.which")
|
||||
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)
|
||||
|
||||
expected = "Funny fortune"
|
||||
packet = fake.fake_packet(message="fortune")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -0,0 +1,89 @@
|
||||
from unittest import mock
|
||||
|
||||
from aprsd.plugins import location as location_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
|
||||
|
||||
class TestLocationPlugin(test_plugin.TestPlugin):
|
||||
|
||||
@mock.patch("aprsd.config.Config.check_option")
|
||||
def test_location_not_enabled_missing_aprs_fi_key(self, mock_check):
|
||||
# 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)
|
||||
expected = "LocationPlugin isn't enabled"
|
||||
packet = fake.fake_packet(message="location")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
def test_location_failed_aprs_fi_location(self, mock_check):
|
||||
# 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)
|
||||
expected = "Failed to fetch aprs.fi location"
|
||||
packet = fake.fake_packet(message="location")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
def test_location_failed_aprs_fi_location_no_entries(self, mock_check):
|
||||
# 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)
|
||||
expected = "Failed to fetch aprs.fi location"
|
||||
packet = fake.fake_packet(message="location")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
|
||||
@mock.patch("time.time")
|
||||
def test_location_unknown_gps(self, mock_time, mock_weather, mock_check_aprs):
|
||||
# When the aprs.fi api key isn't set, then
|
||||
# the LocationPlugin will be disabled.
|
||||
mock_check_aprs.return_value = {
|
||||
"entries": [
|
||||
{
|
||||
"lat": 10,
|
||||
"lng": 11,
|
||||
"lasttime": 10,
|
||||
},
|
||||
],
|
||||
}
|
||||
mock_weather.side_effect = Exception
|
||||
mock_time.return_value = 10
|
||||
fortune = location_plugin.LocationPlugin(self.config)
|
||||
expected = "KFAKE: Unknown Location 0' 10,11 0.0h ago"
|
||||
packet = fake.fake_packet(message="location")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
|
||||
@mock.patch("time.time")
|
||||
def test_location_works(self, mock_time, mock_weather, mock_check_aprs):
|
||||
# When the aprs.fi api key isn't set, then
|
||||
# the LocationPlugin will be disabled.
|
||||
mock_check_aprs.return_value = {
|
||||
"entries": [
|
||||
{
|
||||
"lat": 10,
|
||||
"lng": 11,
|
||||
"lasttime": 10,
|
||||
},
|
||||
],
|
||||
}
|
||||
expected_town = "Appomattox, VA"
|
||||
wx_data = {"location": {"areaDescription": expected_town}}
|
||||
mock_weather.return_value = wx_data
|
||||
mock_time.return_value = 10
|
||||
fortune = location_plugin.LocationPlugin(self.config)
|
||||
expected = f"KFAKE: {expected_town} 0' 10,11 0.0h ago"
|
||||
packet = fake.fake_packet(message="location")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -0,0 +1,185 @@
|
||||
from unittest import mock
|
||||
|
||||
from aprsd import client
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import messaging, packets
|
||||
from aprsd.plugins import notify as notify_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
|
||||
|
||||
DEFAULT_WATCHLIST_CALLSIGNS = [fake.FAKE_FROM_CALLSIGN]
|
||||
|
||||
|
||||
class TestWatchListPlugin(test_plugin.TestPlugin):
|
||||
def setUp(self):
|
||||
self.fromcall = fake.FAKE_FROM_CALLSIGN
|
||||
self.ack = 1
|
||||
|
||||
def _config(
|
||||
self,
|
||||
watchlist_enabled=True,
|
||||
watchlist_alert_callsign=None,
|
||||
watchlist_alert_time_seconds=None,
|
||||
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["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
|
||||
_config["services"]["aprs.fi"]["apiKey"] = "something"
|
||||
|
||||
# Set the watchlist specific config options
|
||||
|
||||
_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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
_config["aprsd"]["watch_list"]["callsigns"] = watchlist_callsigns
|
||||
return _config
|
||||
|
||||
|
||||
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)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="version",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = messaging.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@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)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall="FAKE",
|
||||
message="version",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = messaging.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="version",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = messaging.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@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)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="version",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = messaging.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.client.ClientFactory", autospec=True)
|
||||
@mock.patch("aprsd.packets.WatchList.is_old")
|
||||
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(
|
||||
watchlist_enabled=True,
|
||||
watchlist_callsigns=["WB4BOR"],
|
||||
)
|
||||
self.config_and_init(config=config)
|
||||
plugin = notify_plugin.NotifySeenPlugin(self.config)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall="WB4BOR",
|
||||
message="ping",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = messaging.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.client.ClientFactory", autospec=True)
|
||||
@mock.patch("aprsd.packets.WatchList.is_old")
|
||||
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(
|
||||
watchlist_enabled=True,
|
||||
watchlist_alert_callsign="WB4BOR",
|
||||
watchlist_callsigns=["WB4BOR"],
|
||||
)
|
||||
self.config_and_init(config=config)
|
||||
plugin = notify_plugin.NotifySeenPlugin(self.config)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall="WB4BOR",
|
||||
message="ping",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = messaging.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.client.ClientFactory", autospec=True)
|
||||
@mock.patch("aprsd.packets.WatchList.is_old")
|
||||
def test_callsign_in_watchlist_old_send_alert(self, mock_is_old, mock_factory):
|
||||
client.factory = mock_factory
|
||||
mock_is_old.return_value = True
|
||||
notify_callsign = "KFAKE"
|
||||
fromcall = "WB4BOR"
|
||||
config = self._config(
|
||||
watchlist_enabled=True,
|
||||
watchlist_alert_callsign=notify_callsign,
|
||||
watchlist_callsigns=["WB4BOR"],
|
||||
)
|
||||
self.config_and_init(config=config)
|
||||
plugin = notify_plugin.NotifySeenPlugin(self.config)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall=fromcall,
|
||||
message="ping",
|
||||
msg_number=1,
|
||||
)
|
||||
packet_type = packets.get_packet_type(packet)
|
||||
actual = plugin.filter(packet)
|
||||
msg = f"{fromcall} was just seen by type:'{packet_type}'"
|
||||
|
||||
self.assertIsInstance(actual, messaging.TextMessage)
|
||||
self.assertEqual(fake.FAKE_TO_CALLSIGN, actual.fromcall)
|
||||
self.assertEqual(notify_callsign, actual.tocall)
|
||||
self.assertEqual(msg, actual.message)
|
||||
@@ -0,0 +1,50 @@
|
||||
from unittest import mock
|
||||
|
||||
from aprsd.plugins import ping as ping_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
|
||||
|
||||
class TestPingPlugin(test_plugin.TestPlugin):
|
||||
@mock.patch("time.localtime")
|
||||
def test_ping(self, mock_time):
|
||||
fake_time = mock.MagicMock()
|
||||
h = fake_time.tm_hour = 16
|
||||
m = fake_time.tm_min = 12
|
||||
s = fake_time.tm_sec = 55
|
||||
mock_time.return_value = fake_time
|
||||
|
||||
ping = ping_plugin.PingPlugin(self.config)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="location",
|
||||
msg_number=1,
|
||||
)
|
||||
|
||||
result = ping.filter(packet)
|
||||
self.assertEqual(None, result)
|
||||
|
||||
def ping_str(h, m, s):
|
||||
return (
|
||||
"Pong! "
|
||||
+ str(h).zfill(2)
|
||||
+ ":"
|
||||
+ str(m).zfill(2)
|
||||
+ ":"
|
||||
+ str(s).zfill(2)
|
||||
)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="Ping",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = ping.filter(packet)
|
||||
expected = ping_str(h, m, s)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="ping",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = ping.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -0,0 +1,37 @@
|
||||
from unittest import mock
|
||||
|
||||
from aprsd import messaging
|
||||
from aprsd.plugins import query as query_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
|
||||
|
||||
class TestQueryPlugin(test_plugin.TestPlugin):
|
||||
@mock.patch("aprsd.messaging.MsgTrack.flush")
|
||||
def test_query_flush(self, mock_flush):
|
||||
packet = fake.fake_packet(message="!delete")
|
||||
query = query_plugin.QueryPlugin(self.config)
|
||||
|
||||
expected = "Deleted ALL pending msgs."
|
||||
actual = query.filter(packet)
|
||||
mock_flush.assert_called_once()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.messaging.MsgTrack.restart_delayed")
|
||||
def test_query_restart_delayed(self, mock_restart):
|
||||
track = messaging.MsgTrack()
|
||||
track.data = {}
|
||||
packet = fake.fake_packet(message="!4")
|
||||
query = query_plugin.QueryPlugin(self.config)
|
||||
|
||||
expected = "No pending msgs to resend"
|
||||
actual = query.filter(packet)
|
||||
mock_restart.assert_not_called()
|
||||
self.assertEqual(expected, actual)
|
||||
mock_restart.reset_mock()
|
||||
|
||||
# add a message
|
||||
msg = messaging.TextMessage(self.fromcall, "testing", self.ack)
|
||||
track.add(msg)
|
||||
actual = query.filter(packet)
|
||||
mock_restart.assert_called_once()
|
||||
@@ -0,0 +1,50 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytz
|
||||
|
||||
from aprsd.fuzzyclock import fuzzy
|
||||
from aprsd.plugins import time as time_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
|
||||
|
||||
class TestTimePlugins(test_plugin.TestPlugin):
|
||||
|
||||
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
|
||||
@mock.patch("aprsd.plugins.time.TimePlugin._get_utcnow")
|
||||
def test_time(self, mock_utcnow, mock_localtz):
|
||||
utcnow = pytz.datetime.datetime.utcnow()
|
||||
mock_utcnow.return_value = utcnow
|
||||
tz = pytz.timezone("US/Pacific")
|
||||
mock_localtz.return_value = tz
|
||||
|
||||
gmt_t = pytz.utc.localize(utcnow)
|
||||
local_t = gmt_t.astimezone(tz)
|
||||
|
||||
fake_time = mock.MagicMock()
|
||||
h = int(local_t.strftime("%H"))
|
||||
m = int(local_t.strftime("%M"))
|
||||
fake_time.tm_sec = 13
|
||||
time = time_plugin.TimePlugin(self.config)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="location",
|
||||
msg_number=1,
|
||||
)
|
||||
|
||||
actual = time.filter(packet)
|
||||
self.assertEqual(None, actual)
|
||||
|
||||
cur_time = fuzzy(h, m, 1)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="time",
|
||||
msg_number=1,
|
||||
)
|
||||
local_short_str = local_t.strftime("%H:%M %Z")
|
||||
expected = "{} ({})".format(
|
||||
cur_time,
|
||||
local_short_str,
|
||||
)
|
||||
actual = time.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -0,0 +1,35 @@
|
||||
from unittest import mock
|
||||
|
||||
import aprsd
|
||||
from aprsd.plugins import version as version_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
|
||||
|
||||
class TestVersionPlugin(test_plugin.TestPlugin):
|
||||
@mock.patch("aprsd.plugin.PluginManager.get_plugins")
|
||||
def test_version(self, mock_get_plugins):
|
||||
expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00"
|
||||
version = version_plugin.VersionPlugin(self.config)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="No",
|
||||
msg_number=1,
|
||||
)
|
||||
|
||||
actual = version.filter(packet)
|
||||
self.assertEqual(None, actual)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="version",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = version.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="Version",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = version.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
@@ -0,0 +1,176 @@
|
||||
from unittest import mock
|
||||
|
||||
from aprsd.plugins import weather as weather_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
|
||||
|
||||
class TestUSWeatherPluginPlugin(test_plugin.TestPlugin):
|
||||
|
||||
@mock.patch("aprsd.config.Config.check_option")
|
||||
def test_not_enabled_missing_aprs_fi_key(self, mock_check):
|
||||
# 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)
|
||||
expected = "USWeatherPlugin isn't enabled"
|
||||
packet = fake.fake_packet(message="weather")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
def test_failed_aprs_fi_location(self, mock_check):
|
||||
# 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)
|
||||
expected = "Failed to fetch aprs.fi location"
|
||||
packet = fake.fake_packet(message="weather")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
def test_failed_aprs_fi_location_no_entries(self, mock_check):
|
||||
# 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)
|
||||
expected = "Failed to fetch aprs.fi location"
|
||||
packet = fake.fake_packet(message="weather")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
|
||||
def test_unknown_gps(self, mock_weather, mock_check_aprs):
|
||||
# When the aprs.fi api key isn't set, then
|
||||
# the LocationPlugin will be disabled.
|
||||
mock_check_aprs.return_value = {
|
||||
"entries": [
|
||||
{
|
||||
"lat": 10,
|
||||
"lng": 11,
|
||||
"lasttime": 10,
|
||||
},
|
||||
],
|
||||
}
|
||||
mock_weather.side_effect = Exception
|
||||
wx = weather_plugin.USWeatherPlugin(self.config)
|
||||
expected = "Unable to get weather"
|
||||
packet = fake.fake_packet(message="weather")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
|
||||
def test_working(self, mock_weather, mock_check_aprs):
|
||||
# When the aprs.fi api key isn't set, then
|
||||
# the LocationPlugin will be disabled.
|
||||
mock_check_aprs.return_value = {
|
||||
"entries": [
|
||||
{
|
||||
"lat": 10,
|
||||
"lng": 11,
|
||||
"lasttime": 10,
|
||||
},
|
||||
],
|
||||
}
|
||||
mock_weather.return_value = {
|
||||
"currentobservation": {"Temp": "400"},
|
||||
"data": {
|
||||
"temperature": ["10", "11"],
|
||||
"weather": ["test", "another"],
|
||||
},
|
||||
"time": {"startPeriodName": ["ignored", "sometime"]},
|
||||
}
|
||||
wx = weather_plugin.USWeatherPlugin(self.config)
|
||||
expected = "400F(10F/11F) test. sometime, another."
|
||||
packet = fake.fake_packet(message="weather")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
class TestUSMetarPlugin(test_plugin.TestPlugin):
|
||||
|
||||
@mock.patch("aprsd.config.Config.check_option")
|
||||
def test_not_enabled_missing_aprs_fi_key(self, mock_check):
|
||||
# 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)
|
||||
expected = "USMetarPlugin isn't enabled"
|
||||
packet = fake.fake_packet(message="metar")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
def test_failed_aprs_fi_location(self, mock_check):
|
||||
# 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)
|
||||
expected = "Failed to fetch aprs.fi location"
|
||||
packet = fake.fake_packet(message="metar")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
def test_failed_aprs_fi_location_no_entries(self, mock_check):
|
||||
# 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)
|
||||
expected = "Failed to fetch aprs.fi location"
|
||||
packet = fake.fake_packet(message="metar")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@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)
|
||||
expected = "Unable to find station METAR"
|
||||
packet = fake.fake_packet(message="metar KPAO")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_weather_gov_metar")
|
||||
def test_airport_works(self, mock_metar):
|
||||
|
||||
class Response:
|
||||
text = '{"properties": {"rawMessage": "BOGUSMETAR"}}'
|
||||
mock_metar.return_value = Response()
|
||||
|
||||
wx = weather_plugin.USMetarPlugin(self.config)
|
||||
expected = "BOGUSMETAR"
|
||||
packet = fake.fake_packet(message="metar KPAO")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.plugin_utils.get_weather_gov_metar")
|
||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
|
||||
def test_metar_works(self, mock_wx_for_gps, mock_check_aprs, mock_metar):
|
||||
mock_wx_for_gps.return_value = {
|
||||
"location": {"metar": "BOGUSMETAR"},
|
||||
}
|
||||
|
||||
class Response:
|
||||
text = '{"properties": {"rawMessage": "BOGUSMETAR"}}'
|
||||
|
||||
mock_check_aprs.return_value = {
|
||||
"entries": [
|
||||
{
|
||||
"lat": 10,
|
||||
"lng": 11,
|
||||
"lasttime": 10,
|
||||
},
|
||||
],
|
||||
}
|
||||
mock_metar.return_value = Response()
|
||||
|
||||
wx = weather_plugin.USMetarPlugin(self.config)
|
||||
expected = "BOGUSMETAR"
|
||||
packet = fake.fake_packet(message="metar")
|
||||
actual = wx.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
+27
-185
@@ -1,33 +1,44 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytz
|
||||
|
||||
import aprsd
|
||||
from aprsd import config, messaging, packets, stats
|
||||
from aprsd.fuzzyclock import fuzzy
|
||||
from aprsd.plugins import fortune as fortune_plugin
|
||||
from aprsd.plugins import ping as ping_plugin
|
||||
from aprsd.plugins import query as query_plugin
|
||||
from aprsd.plugins import time as time_plugin
|
||||
from aprsd.plugins import version as version_plugin
|
||||
from aprsd import config as aprsd_config
|
||||
from aprsd import messaging, packets, stats
|
||||
|
||||
from . import fake
|
||||
|
||||
|
||||
class TestPlugin(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.fromcall = fake.FAKE_FROM_CALLSIGN
|
||||
self.ack = 1
|
||||
self.config = config.DEFAULT_CONFIG_DICT
|
||||
self.config["ham"]["callsign"] = self.fromcall
|
||||
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
|
||||
self.config_and_init()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
stats.APRSDStats._instance = None
|
||||
packets.WatchList._instance = None
|
||||
packets.SeenList._instance = None
|
||||
messaging.MsgTrack._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)
|
||||
messaging.MsgTrack(config=self.config)
|
||||
|
||||
|
||||
class TestPluginBase(TestPlugin):
|
||||
|
||||
@mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process")
|
||||
def test_base_plugin_no_threads(self, mock_process):
|
||||
p = fake.FakeBaseNoThreadsPlugin(self.config)
|
||||
@@ -51,8 +62,9 @@ class TestPlugin(unittest.TestCase):
|
||||
|
||||
@mock.patch.object(fake.FakeBaseThreadsPlugin, "create_threads")
|
||||
def test_base_plugin_threads_created(self, mock_create):
|
||||
fake.FakeBaseThreadsPlugin(self.config)
|
||||
p = fake.FakeBaseThreadsPlugin(self.config)
|
||||
mock_create.assert_called_once()
|
||||
p.stop_threads()
|
||||
|
||||
def test_base_plugin_threads(self):
|
||||
p = fake.FakeBaseThreadsPlugin(self.config)
|
||||
@@ -122,173 +134,3 @@ class TestPlugin(unittest.TestCase):
|
||||
expected = fake.FAKE_MESSAGE_TEXT
|
||||
actual = p.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
class TestFortunePlugin(TestPlugin):
|
||||
@mock.patch("shutil.which")
|
||||
def test_fortune_fail(self, mock_which):
|
||||
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||
mock_which.return_value = None
|
||||
expected = "Fortune command not installed"
|
||||
packet = fake.fake_packet(message="fortune")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("subprocess.check_output")
|
||||
@mock.patch("shutil.which")
|
||||
def test_fortune_success(self, mock_which, mock_output):
|
||||
fortune = fortune_plugin.FortunePlugin(self.config)
|
||||
mock_which.return_value = "/usr/bin/games"
|
||||
|
||||
mock_output.return_value = "Funny fortune"
|
||||
|
||||
expected = "Funny fortune"
|
||||
packet = fake.fake_packet(message="fortune")
|
||||
actual = fortune.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
class TestQueryPlugin(TestPlugin):
|
||||
@mock.patch("aprsd.messaging.MsgTrack.flush")
|
||||
def test_query_flush(self, mock_flush):
|
||||
packet = fake.fake_packet(message="!delete")
|
||||
query = query_plugin.QueryPlugin(self.config)
|
||||
|
||||
expected = "Deleted ALL pending msgs."
|
||||
actual = query.filter(packet)
|
||||
mock_flush.assert_called_once()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.messaging.MsgTrack.restart_delayed")
|
||||
def test_query_restart_delayed(self, mock_restart):
|
||||
track = messaging.MsgTrack()
|
||||
track.data = {}
|
||||
packet = fake.fake_packet(message="!4")
|
||||
query = query_plugin.QueryPlugin(self.config)
|
||||
|
||||
expected = "No pending msgs to resend"
|
||||
actual = query.filter(packet)
|
||||
mock_restart.assert_not_called()
|
||||
self.assertEqual(expected, actual)
|
||||
mock_restart.reset_mock()
|
||||
|
||||
# add a message
|
||||
msg = messaging.TextMessage(self.fromcall, "testing", self.ack)
|
||||
track.add(msg)
|
||||
actual = query.filter(packet)
|
||||
mock_restart.assert_called_once()
|
||||
|
||||
|
||||
class TestTimePlugins(TestPlugin):
|
||||
@mock.patch("aprsd.plugins.time.TimePlugin._get_local_tz")
|
||||
@mock.patch("aprsd.plugins.time.TimePlugin._get_utcnow")
|
||||
def test_time(self, mock_utcnow, mock_localtz):
|
||||
utcnow = pytz.datetime.datetime.utcnow()
|
||||
mock_utcnow.return_value = utcnow
|
||||
tz = pytz.timezone("US/Pacific")
|
||||
mock_localtz.return_value = tz
|
||||
|
||||
gmt_t = pytz.utc.localize(utcnow)
|
||||
local_t = gmt_t.astimezone(tz)
|
||||
|
||||
fake_time = mock.MagicMock()
|
||||
h = int(local_t.strftime("%H"))
|
||||
m = int(local_t.strftime("%M"))
|
||||
fake_time.tm_sec = 13
|
||||
time = time_plugin.TimePlugin(self.config)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="location",
|
||||
msg_number=1,
|
||||
)
|
||||
|
||||
actual = time.filter(packet)
|
||||
self.assertEqual(None, actual)
|
||||
|
||||
cur_time = fuzzy(h, m, 1)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="time",
|
||||
msg_number=1,
|
||||
)
|
||||
local_short_str = local_t.strftime("%H:%M %Z")
|
||||
expected = "{} ({})".format(
|
||||
cur_time,
|
||||
local_short_str,
|
||||
)
|
||||
actual = time.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
class TestPingPlugin(TestPlugin):
|
||||
@mock.patch("time.localtime")
|
||||
def test_ping(self, mock_time):
|
||||
fake_time = mock.MagicMock()
|
||||
h = fake_time.tm_hour = 16
|
||||
m = fake_time.tm_min = 12
|
||||
s = fake_time.tm_sec = 55
|
||||
mock_time.return_value = fake_time
|
||||
|
||||
ping = ping_plugin.PingPlugin(self.config)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="location",
|
||||
msg_number=1,
|
||||
)
|
||||
|
||||
result = ping.filter(packet)
|
||||
self.assertEqual(None, result)
|
||||
|
||||
def ping_str(h, m, s):
|
||||
return (
|
||||
"Pong! "
|
||||
+ str(h).zfill(2)
|
||||
+ ":"
|
||||
+ str(m).zfill(2)
|
||||
+ ":"
|
||||
+ str(s).zfill(2)
|
||||
)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="Ping",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = ping.filter(packet)
|
||||
expected = ping_str(h, m, s)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="ping",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = ping.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
class TestVersionPlugin(TestPlugin):
|
||||
@mock.patch("aprsd.plugin.PluginManager.get_plugins")
|
||||
def test_version(self, mock_get_plugins):
|
||||
expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00"
|
||||
version = version_plugin.VersionPlugin(self.config)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="No",
|
||||
msg_number=1,
|
||||
)
|
||||
|
||||
actual = version.filter(packet)
|
||||
self.assertEqual(None, actual)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="version",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = version.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="Version",
|
||||
msg_number=1,
|
||||
)
|
||||
actual = version.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
minversion = 2.9.0
|
||||
skipdist = True
|
||||
skip_missing_interpreters = true
|
||||
envlist = pre-commit,pep8,py{36,37,38}
|
||||
envlist = pre-commit,pep8,py{36,37,38,39}
|
||||
|
||||
# Activate isolated build environment. tox will use a virtual environment
|
||||
# to build a source distribution from the source tree. For build tools and
|
||||
@@ -10,8 +10,11 @@ envlist = pre-commit,pep8,py{36,37,38}
|
||||
isolated_build = true
|
||||
|
||||
[testenv]
|
||||
setenv = _PYTEST_SETUP_SKIP_APRSD_DEP=1
|
||||
coverage: _APRSD_TOX_CMD=coverage run -m pytest
|
||||
description = Run unit-testing
|
||||
setenv =
|
||||
_PYTEST_SETUP_SKIP_APRSD_DEP=1
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
PYTHONUNBUFFERED=1
|
||||
usedevelop = True
|
||||
install_command = pip install {opts} {packages}
|
||||
extras = tests
|
||||
@@ -20,18 +23,10 @@ deps = coverage: coverage
|
||||
-r{toxinidir}/dev-requirements.txt
|
||||
pytestmain: git+https://github.com/pytest-dev/pytest.git@main
|
||||
commands =
|
||||
{env:_APRSD_TOX_CMD:pytest} {posargs}
|
||||
pytest -v --cov-report term-missing --cov=aprsd {posargs}
|
||||
coverage: coverage report -m
|
||||
coverage: coverage xml
|
||||
|
||||
[pytest]
|
||||
minversion=2.0
|
||||
testpaths = tests
|
||||
#--pyargs --doctest-modules --ignore=.tox
|
||||
addopts=-r a
|
||||
filterwarnings =
|
||||
error
|
||||
|
||||
[testenv:docs]
|
||||
skip_install = true
|
||||
deps =
|
||||
@@ -78,8 +73,8 @@ exclude = .venv,.git,.tox,dist,doc,.ropeproject
|
||||
python =
|
||||
3.6: py36, pep8
|
||||
3.7: py38, pep8
|
||||
3.8: py38, pep8, type-check, docs
|
||||
3.9: py39
|
||||
3.8: py38, pep8
|
||||
3.9: py39, pep8, type-check, docs
|
||||
|
||||
[testenv:fmt]
|
||||
# This will reformat your code to comply with pep8
|
||||
|
||||
Reference in New Issue
Block a user