1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-18 01:23:56 -04:00

Compare commits

...

65 Commits

Author SHA1 Message Date
hemna 6257c9ea90 FIX: logging exceptions
This patch fixes the logging of exceptions in the email
plugin.
2022-01-26 14:39:14 -05:00
hemna b00c8db3d6 Updated build and run for rich lib 2022-01-08 09:41:17 -05:00
hemna 79270f95be update build for 2.5.8 2022-01-08 09:29:26 -05:00
hemna 29a60b7ed0 For 2.5.8 2022-01-07 15:19:44 -05:00
hemna e8100d8777 Removed debug code 2022-01-07 15:17:16 -05:00
hemna 764730c123 Updated list-plugins
This patch updates the README.rst with the new format for
`aprsd list-plugins`.
2021-12-15 10:48:16 -05:00
hemna 610e40aecd Renamed virtualenv dir to .aprsd-venv
This helps with shell prompts showing the name of the venv.
When you have multiple venv environments on your system, naming then
helps to identify which one you are actively using.
2021-12-15 10:45:53 -05:00
hemna 2f6e7e17e8 Added unit tests for dev test-plugin
Also added a check to make sure that the aprs_login
parameter is passed in for use as the fromcallsign.
2021-12-12 16:35:26 -05:00
hemna a7bbde4a43 Send Message command defaults to config
The APRS_LOGIN and APRS_PASSWORD arguments now fallback
to the config file if it exists.

First it checks the passed in parameters, then checks the
environement vars, then checks the parsed config to find the
login and password.

This patch also adds unit tests for the send-message command to
check the fallback.
2021-12-12 16:13:08 -05:00
hemna 7530bcf55c Updated Changelog 2021-12-11 07:59:50 -05:00
hemna ab37a5e7a7 Merge pull request #79 from craigerl/fix_kiss_is_enabled
Fixed an KISS config disabled issue
2021-12-11 07:56:47 -05:00
hemna 3b9970c0e7 Fixed an KISS config disabled issue
This patch fixes a small bug when both KISS interfaces are disabled.
2021-12-11 07:46:43 -05:00
hemna e57a2e2ffc Fixed a bug with multiple notify plugins enabled
This patch fixes an issue with the processing of packets
and updateing the watchlist.  Previously after the
notify plugin processed the packet it would update the watchlist.
This doesn't work when there are more than 1 notify plugins
enabled, only the first notify plugin seeing the packet will
recognize that the callsign is old.
2021-12-10 14:20:57 -05:00
hemna 6a1cea63e4 Merge pull request #77 from craigerl/logs
Unify the logging to file and stdout
2021-12-10 11:12:48 -05:00
hemna 592b328956 Unify the logging to file and stdout
This patch updates the logging facility to ensure that
logging to a file works even when --quiet mode is selected.
Also update the listen and list-plugins command to show
a console.status line while waiting for results to come in.
2021-12-10 10:49:09 -05:00
hemna 450bacfe99 Merge pull request #76 from craigerl/list-plugins
Added new feature to list-plugins command
2021-12-09 09:44:49 -05:00
hemna cd62db95c1 Added new feature to list-plugins command
This patch updates the ouput of the list-plugins command.
This also adds the ability to show the available plugins
to install that are published packages on pypi.org.

This also shows the list of installed packages from pypi.org
2021-12-08 17:16:17 -05:00
hemna 28b54c330d more README.rst cleanup 2021-12-07 15:22:08 -05:00
hemna 7c653cc100 Updated README examples
The examples in the README.rst were painfully old.
2021-12-07 15:18:27 -05:00
hemna b7791eb4fa Changelog 2021-12-07 15:05:34 -05:00
hemna 440c8d54ad Tightened up the packet logging 2021-12-07 15:00:38 -05:00
hemna bcc1b4e309 Merge pull request #75 from craigerl/unittests
Unittests
2021-12-07 13:37:02 -05:00
hemna 8ea00e9888 Added unit tests for USWeatherPlugin, USMetarPlugin 2021-12-07 13:31:58 -05:00
hemna 5d6ac5cf31 Added test_location to test LocationPlugin 2021-12-07 12:38:12 -05:00
hemna e0e75149a9 Updated pytest output
This patch changes tox.ini to update the output for the unit test
runs.
2021-12-07 11:57:01 -05:00
hemna a5184fb98c Added py39 to tox for tests 2021-12-07 11:35:18 -05:00
hemna 0ad791bdd9 Added NotifyPlugin unit tests and more
This patch restructures the unit tests for plugins.
This also adds unit tests for the NotifyPlugin
2021-12-07 11:25:14 -05:00
hemna 96cc07d15f Small cleanup on packet logging
This patch reduces some of the leading whitespace
to the message/packet logging to the log file.
2021-12-06 14:35:49 -05:00
hemna d3dd08714b Reduced the APRSIS connection reset to 2 minutes
The time in which the KeepAlive Thread would reset the APRS-IS
socket connection used to be 5 minutes.   This patch changes
that to 2 minutes.
2021-12-06 14:34:22 -05:00
hemna 055835cb3c Fixed the NotifyPlugin
The watchlist notify plugin is supposed to send an APRS message
to the configured callsign.  This patch makes sure that the
message is sent to the notify_callsign
2021-12-06 14:11:34 -05:00
hemna ff8bf02e26 Merge pull request #74 from craigerl/rich_logging
Rich logging
2021-12-03 09:32:25 -05:00
hemna b5b286e75c Fixed some pep8 errors 2021-12-03 09:10:33 -05:00
hemna 1233137caf Add tracing for dev command
This patch enables tracing output in the log for the dev
test-plugin command
2021-12-03 08:53:08 -05:00
hemna 1d5f76defc Added python rich library based logging.
The python rich library is extensive and has a really nice
log format that is easier to read and has built in formatting
and coloring of the log output.

To enable rich logging add rich_logging: True in the config file.
2021-12-03 08:05:03 -05:00
hemna 950c62f49b Merge pull request #73 from emresaglam/loglevel
Added LOG_LEVEL env variable for the docker
2021-12-03 08:02:15 -05:00
Emre Saglam 7aaa002a0e Added LOG_LEVEL env variable for the docker 2021-12-02 17:08:41 -08:00
hemna e27887db1a Update requirements to use aprslib 0.7.0
aprslib 0.7.0 has a few aprs packet parsing fixes.

https://github.com/rossengeorgiev/aprs-python/pull/66

Support for the 'more recent' reply/ack msg format from 1999
2021-11-28 10:47:12 -05:00
hemna 5e50792e80 fixed the failure during loading for objectstore
This patch fixes a silent failure of loading data from the objectstore
2021-11-13 15:07:28 -05:00
hemna deec249c45 updated docker build 2021-11-13 10:01:38 -05:00
hemna ade3c49e93 Updated Changelog 2021-11-13 10:00:40 -05:00
hemna 6fb610582d Fixed dev command missing initialization
This patch fixes a few issues when running test-plugin command.
It was missing some initialization of the stats and packets classes.
2021-11-13 09:56:19 -05:00
hemna bda2ef00dd Fix admin logging tab 2021-11-12 12:17:45 -05:00
hemna 446484e631 Added new list-plugins command
This patch adds the new list-plugins command that shows the
list of built in plugins for APRSD.
2021-11-12 11:36:22 -05:00
hemna a8a6b1aa07 Don't require check-version command to have a config
This patch removes the need for check-version to have a
config file.
2021-11-12 10:23:27 -05:00
hemna 8842fb1b44 Healthcheck command doesn't need the aprsd.yml config
This patch updates the healthcheck command to not require
the aprsd.yml config file to exist.   The healthcheck
calls a running aprsd, collects the stats to determine if it's
healthy.
2021-11-10 11:52:51 -05:00
hemna 152132b0ed Fix test failures 2021-11-10 11:51:21 -05:00
hemna 7787dc1be4 Removed requirement for aprs.fi key
This removed the requirement of running APRSD for specifying
the aprs.fi key in the config file.  The plugins that need the
key have been updated to set enabled = False when the key is missing.
2021-11-10 11:01:10 -05:00
hemna 10e34d8634 Updated Changelog 2021-11-09 15:06:40 -05:00
hemna 9469410929 Removed stock plugin. 2021-11-09 15:02:54 -05:00
hemna 998bc32c27 Merge pull request #72 from craigerl/remove_stock_plugin
Removed the stock plugin
2021-11-09 14:59:03 -05:00
hemna 88db485eb4 Removed the stock plugin
This patch removed the built in stock plugin from APRSD.
This helps clean up the requirement tree from the yfinance
python module that pulled in a lot of other requirements.

The stock plugin is it's own separate repo and module now.

https://github.com/hemna/aprsd-stock-plugin

https://pypi.org/project/aprsd-stock-plugin/
2021-11-09 14:53:20 -05:00
hemna 5d17809895 Updated for v2.5.0 2021-11-09 10:31:29 -05:00
hemna 059cc86a11 Updated Dockerfile's and build script for docker 2021-11-09 08:15:16 -05:00
hemna ffdd1e47b2 Merge pull request #71 from craigerl/refactor_cli
Refactor cli
2021-11-09 08:07:19 -05:00
hemna cdcb98e438 Cleaned up some verbose output & colorized output
Some commands now have some color if the shell detects it supports it.
2021-11-08 12:18:23 -05:00
hemna 89727e2b8e Reworked all the common arguments
This patch reworks all the common arguments for the commands
and subcommands

--loglevel
--config_file
--quiet

These are all now processed in 1 place.
2021-11-08 11:52:41 -05:00
hemna 617973f561 Fixed test-plugin 2021-11-05 16:40:07 -04:00
hemna 9187b9781a Ensure common params are honored 2021-11-05 16:26:24 -04:00
hemna 8287c09ce5 pep8 2021-11-05 14:38:23 -04:00
hemna 82def598f0 Added healthcheck to the cmds
this patch moves the healthcheck to it's own command.
aprsd healthcheck
2021-11-05 14:21:36 -04:00
hemna 3463c6eb96 Removed the need for FROMCALL in dev test-plugin
We already use the env var for APRS_LOGIN, so that is now
used for the test-plugin command.
Also cleaned up some help text
2021-11-05 14:05:24 -04:00
hemna 2ead6a97da Pep8 failures 2021-11-05 13:42:27 -04:00
hemna 7d0006b0a6 Refactor the cli
This patch refactors the cli to incorporate
the dev, send-message, listen commands into the main aprsd app.
This also moves the command line completion installer/show into
it's own subgroup.
2021-11-05 13:36:33 -04:00
hemna 30df452e00 Updated Changelog for 4.2.3 2021-11-05 10:51:22 -04:00
hemna 49f3ea8339 Fixed a problem with send-message command
This patch fixes a problem with the packets object
not being initialized correctly for the send-message command
from the command line.

Also adds the --wait-response option for send-message, which by
default is now False
2021-11-05 10:39:47 -04:00
64 changed files with 2727 additions and 2042 deletions
+90
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+88
View File
@@ -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)
+2
View File
@@ -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):
View File
View File
+36
View File
@@ -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}")
+134
View File
@@ -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()
+80
View File
@@ -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)
+242
View File
@@ -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)
+164
View File
@@ -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")
+169
View File
@@ -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()
+120
View File
@@ -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
View File
@@ -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
View File
@@ -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()
-84
View File
@@ -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
View File
@@ -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):
-233
View File
@@ -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
View File
@@ -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()
+81
View File
@@ -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)
View File
+160
View File
@@ -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
View File
@@ -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
View File
@@ -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))
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 -7
View File
@@ -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,
+5 -9
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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):
-51
View File
@@ -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
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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?"
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1
View File
@@ -9,3 +9,4 @@ services:
environment:
- TZ=America/New_York
- APRS_PLUGINS=aprsd-slack-plugin>=1.0.2
- LOG_LEVEL=ERROR
+5 -2
View File
@@ -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
View File
@@ -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
+1 -5
View File
@@ -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
View File
+58
View File
@@ -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
+94
View File
@@ -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
View File
@@ -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
View File
+28
View File
@@ -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)
+89
View File
@@ -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)
+185
View File
@@ -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)
+50
View File
@@ -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)
+37
View File
@@ -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()
+50
View File
@@ -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)
+35
View File
@@ -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)
+176
View File
@@ -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
View File
@@ -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)
+9 -14
View File
@@ -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