mirror of
https://github.com/caronc/apprise.git
synced 2024-11-21 15:43:27 +01:00
APRS (Automated Packet Reporting System) Ham Radio plugin (#1005)
This commit is contained in:
parent
76831f9a8b
commit
d7166a270f
1
.gitignore
vendored
1
.gitignore
vendored
@ -69,3 +69,4 @@ target/
|
|||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
.settings
|
.settings
|
||||||
|
.DS_Store
|
||||||
|
741
apprise/plugins/NotifyAprs.py
Normal file
741
apprise/plugins/NotifyAprs.py
Normal file
@ -0,0 +1,741 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# BSD 2-Clause License
|
||||||
|
#
|
||||||
|
# Apprise - Push Notification Library.
|
||||||
|
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
#
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
#
|
||||||
|
# To use this plugin, you need to be a licensed ham radio operator
|
||||||
|
#
|
||||||
|
# Plugin constraints:
|
||||||
|
#
|
||||||
|
# - message length = 67 chars max.
|
||||||
|
# - message content = ASCII 7 bit
|
||||||
|
# - APRS messages will be sent without msg ID, meaning that
|
||||||
|
# ham radio operators cannot acknowledge them
|
||||||
|
# - Bring your own APRS-IS passcode. If you don't know what
|
||||||
|
# this is or how to get it, then this plugin is not for you
|
||||||
|
# - Do NOT change the Device/ToCall ID setting UNLESS this
|
||||||
|
# module is used outside of Apprise. This identifier helps
|
||||||
|
# the ham radio community with determining the software behind
|
||||||
|
# a given APRS message.
|
||||||
|
# - With great (ham radio) power comes great responsibility; do
|
||||||
|
# not use this plugin for spamming other ham radio operators
|
||||||
|
|
||||||
|
#
|
||||||
|
# In order to digest text input which is not in plain English,
|
||||||
|
# users can install the optional 'unidecode' package as part
|
||||||
|
# of their venv environment. Details: see plugin description
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# You're done at this point, you only need to know your user/pass that
|
||||||
|
# you signed up with.
|
||||||
|
|
||||||
|
# The following URLs would be accepted by Apprise:
|
||||||
|
# - aprs://{user}:{password}@{callsign}
|
||||||
|
# - aprs://{user}:{password}@{callsign1}/{callsign2}
|
||||||
|
|
||||||
|
# Optional parameters:
|
||||||
|
# - locale --> APRS-IS target server to connect with
|
||||||
|
# Default: EURO --> 'euro.aprs2.net'
|
||||||
|
# Details: https://www.aprs2.net/
|
||||||
|
|
||||||
|
#
|
||||||
|
# APRS message format specification:
|
||||||
|
# http://www.aprs.org/doc/APRS101.PDF
|
||||||
|
#
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
from itertools import chain
|
||||||
|
from .NotifyBase import NotifyBase
|
||||||
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
from ..URLBase import PrivacyMode
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..utils import is_call_sign
|
||||||
|
from ..utils import parse_call_sign
|
||||||
|
from .. import __version__
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Fixed APRS-IS server locales
|
||||||
|
# Default is 'EURO'
|
||||||
|
# See https://www.aprs2.net/ for details
|
||||||
|
# Select the rotating server in case you
|
||||||
|
# don"t care about a specific locale
|
||||||
|
APRS_LOCALES = {
|
||||||
|
"NOAM": "noam.aprs2.net",
|
||||||
|
"SOAM": "soam.aprs2.net",
|
||||||
|
"EURO": "euro.aprs2.net",
|
||||||
|
"ASIA": "asia.aprs2.net",
|
||||||
|
"AUNZ": "aunz.aprs2.net",
|
||||||
|
"ROTA": "rotate.aprs2.net",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Identify all unsupported characters
|
||||||
|
APRS_BAD_CHARMAP = {
|
||||||
|
r"Ä": "Ae",
|
||||||
|
r"Ö": "Oe",
|
||||||
|
r"Ü": "Ue",
|
||||||
|
r"ä": "ae",
|
||||||
|
r"ö": "oe",
|
||||||
|
r"ü": "ue",
|
||||||
|
r"ß": "ss",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Our compiled mapping of bad characters
|
||||||
|
APRS_COMPILED_MAP = re.compile(
|
||||||
|
r'(' + '|'.join(APRS_BAD_CHARMAP.keys()) + r')')
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyAprs(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for APRS Notifications via APRS-IS
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = "Aprs"
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = "https://www.aprs2.net/"
|
||||||
|
|
||||||
|
# The default secure protocol
|
||||||
|
secure_protocol = "aprs"
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = "https://github.com/caronc/apprise/wiki/Notify_aprs"
|
||||||
|
|
||||||
|
# APRS default port, supported by all core servers
|
||||||
|
# Details: https://www.aprs-is.net/Connecting.aspx
|
||||||
|
notify_port = 10152
|
||||||
|
|
||||||
|
# The maximum length of the APRS message body
|
||||||
|
body_maxlen = 67
|
||||||
|
|
||||||
|
# Apprise APRS Device ID / TOCALL ID
|
||||||
|
# This is a FIXED value which is associated with this plugin.
|
||||||
|
# Its value MUST NOT be changed. If you use this APRS plugin
|
||||||
|
# code OUTSIDE of Apprise, please request your own TOCALL ID.
|
||||||
|
# Details: see https://github.com/aprsorg/aprs-deviceid
|
||||||
|
#
|
||||||
|
# Do NOT use the generic "APRS" TOCALL ID !!!!!
|
||||||
|
#
|
||||||
|
device_id = "APPRIS"
|
||||||
|
|
||||||
|
# A title can not be used for APRS Messages. Setting this to zero will
|
||||||
|
# cause any title (if defined) to get placed into the message body.
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Helps to reduce the number of login-related errors where the
|
||||||
|
# APRS-IS server "isn't ready yet". If we try to receive the rx buffer
|
||||||
|
# without this grace perid in place, we may receive "incomplete" responses
|
||||||
|
# where the login response lacks information. In case you receive too many
|
||||||
|
# "Rx: APRS-IS msg is too short - needs to have at least two lines" error
|
||||||
|
# messages, you might want to increase this value to a larger time span
|
||||||
|
# Per previous experience, do not use values lower than 0.5 (seconds)
|
||||||
|
request_rate_per_sec = 0.8
|
||||||
|
|
||||||
|
# Encoding of retrieved content
|
||||||
|
aprs_encoding = 'latin-1'
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = ("{schema}://{user}:{password}@{targets}",)
|
||||||
|
|
||||||
|
# Define our template tokens
|
||||||
|
template_tokens = dict(
|
||||||
|
NotifyBase.template_tokens,
|
||||||
|
**{
|
||||||
|
"user": {
|
||||||
|
"name": _("User Name"),
|
||||||
|
"type": "string",
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": _("Password"),
|
||||||
|
"type": "string",
|
||||||
|
"private": True,
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"target_callsign": {
|
||||||
|
"name": _("Target Callsign"),
|
||||||
|
"type": "string",
|
||||||
|
"regex": (
|
||||||
|
r"^[a-z0-9]{2,5}(-[a-z0-9]{1,2})?$",
|
||||||
|
"i",
|
||||||
|
),
|
||||||
|
"map_to": "targets",
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"name": _("Targets"),
|
||||||
|
"type": "list:string",
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(
|
||||||
|
NotifyBase.template_args,
|
||||||
|
**{
|
||||||
|
"to": {
|
||||||
|
"name": _("Target Callsign"),
|
||||||
|
"type": "string",
|
||||||
|
"map_to": "targets",
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"name": _("Locale"),
|
||||||
|
"type": "choice:string",
|
||||||
|
"values": APRS_LOCALES,
|
||||||
|
"default": "EURO",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, targets=None, locale=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize APRS Object
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
# Our (future) socket sobject
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
# Parse our targets
|
||||||
|
self.targets = list()
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check if the user has provided credentials
|
||||||
|
"""
|
||||||
|
if not (self.user and self.password):
|
||||||
|
msg = "An APRS user/pass was not provided."
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check if the user tries to use a read-only access
|
||||||
|
to APRS-IS. We need to send content, meaning that
|
||||||
|
read-only access will not work
|
||||||
|
"""
|
||||||
|
if self.password == "-1":
|
||||||
|
msg = "APRS read-only passwords are not supported."
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check if the password is numeric
|
||||||
|
"""
|
||||||
|
if not self.password.isnumeric():
|
||||||
|
msg = "Invalid APRS-IS password"
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Convert given user name (FROM callsign) and
|
||||||
|
device ID to to uppercase
|
||||||
|
"""
|
||||||
|
self.user = self.user.upper()
|
||||||
|
self.device_id = self.device_id.upper()
|
||||||
|
|
||||||
|
"""
|
||||||
|
Check if the user has provided a locale for the
|
||||||
|
APRS-IS-server and validate it, if necessary
|
||||||
|
"""
|
||||||
|
if locale:
|
||||||
|
if locale.upper() not in APRS_LOCALES:
|
||||||
|
msg = (
|
||||||
|
"Unsupported APRS-IS server locale. "
|
||||||
|
"Received: {}. Valid: {}".format(
|
||||||
|
locale, ", ".join(str(x) for x in APRS_LOCALES.keys())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Set the transmitter group
|
||||||
|
self.locale = \
|
||||||
|
NotifyAprs.template_args["locale"]["default"] \
|
||||||
|
if not locale else locale.upper()
|
||||||
|
|
||||||
|
# Used for URL generation afterwards only
|
||||||
|
self.invalid_targets = list()
|
||||||
|
|
||||||
|
for target in parse_call_sign(targets):
|
||||||
|
# Validate targets and drop bad ones
|
||||||
|
# We just need to know if the call sign (including SSID, if
|
||||||
|
# provided) is valid and can then process the input as is
|
||||||
|
result = is_call_sign(target)
|
||||||
|
if not result:
|
||||||
|
self.logger.warning(
|
||||||
|
"Dropping invalid Amateur radio call sign ({}).".format(
|
||||||
|
target
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.invalid_targets.append(target.upper())
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store entry
|
||||||
|
self.targets.append(target.upper())
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def socket_close(self):
|
||||||
|
"""
|
||||||
|
Closes the socket connection whereas present
|
||||||
|
"""
|
||||||
|
if self.sock:
|
||||||
|
try:
|
||||||
|
self.sock.close()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# No worries if socket exception thrown on close()
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def socket_open(self):
|
||||||
|
"""
|
||||||
|
Establishes the connection to the APRS-IS
|
||||||
|
socket server
|
||||||
|
"""
|
||||||
|
self.logger.debug(
|
||||||
|
"Creating socket connection with APRS-IS {}:{}".format(
|
||||||
|
APRS_LOCALES[self.locale], self.notify_port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.sock = socket.create_connection(
|
||||||
|
(APRS_LOCALES[self.locale], self.notify_port),
|
||||||
|
self.socket_connect_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
except ConnectionError as e:
|
||||||
|
self.logger.debug("Socket Exception socket_open: %s", str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
self.logger.debug("Socket Exception socket_open: %s", str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except socket.timeout as e:
|
||||||
|
self.logger.debug(
|
||||||
|
"Socket Timeout Exception socket_open: %s", str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug("General Exception socket_open: %s", str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
# We are connected.
|
||||||
|
# getpeername() is not supported by every OS. Therefore,
|
||||||
|
# we MAY receive an exception even though we are
|
||||||
|
# connected successfully.
|
||||||
|
try:
|
||||||
|
# Get the physical host/port of the server
|
||||||
|
host, port = self.sock.getpeername()
|
||||||
|
# and create debug info
|
||||||
|
self.logger.debug("Connected to {}:{}".format(host, port))
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
# Seens as if we are running on an operating
|
||||||
|
# system that does not support getpeername()
|
||||||
|
# Create a minimal log file entry
|
||||||
|
self.logger.debug("Connected to APRS-IS")
|
||||||
|
|
||||||
|
# Return success
|
||||||
|
return True
|
||||||
|
|
||||||
|
def aprsis_login(self):
|
||||||
|
"""
|
||||||
|
Generate the APRS-IS login string, send it to the server
|
||||||
|
and parse the response
|
||||||
|
|
||||||
|
Returns True/False wrt whether the login was successful
|
||||||
|
"""
|
||||||
|
self.logger.debug("socket_login: init")
|
||||||
|
|
||||||
|
# Check if we are connected
|
||||||
|
if not self.sock:
|
||||||
|
self.logger.warning("socket_login: Not connected to APRS-IS")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# APRS-IS login string, see https://www.aprs-is.net/Connecting.aspx
|
||||||
|
login_str = "user {0} pass {1} vers apprise {2}\r\n".format(
|
||||||
|
self.user, self.password, __version__
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send the data & abort in case of error
|
||||||
|
if not self.socket_send(login_str):
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: Login to APRS-IS unsuccessful,"
|
||||||
|
" exception occurred"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
rx_buf = self.socket_receive(len(login_str) + 100)
|
||||||
|
# Abort the remaining process in case an error has occurred
|
||||||
|
if not rx_buf:
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: Login to APRS-IS "
|
||||||
|
"unsuccessful, exception occurred"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# APRS-IS sends at least two lines of data
|
||||||
|
# The data that we need is in line #2 so
|
||||||
|
# let's split the content and see what we have
|
||||||
|
rx_lines = rx_buf.splitlines()
|
||||||
|
if len(rx_lines) < 2:
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: APRS-IS msg is too short"
|
||||||
|
" - needs to have at least two lines"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Now split the 2nd line's content and extract
|
||||||
|
# both call sign and login status
|
||||||
|
try:
|
||||||
|
_, _, callsign, status, _ = rx_lines[1].split(" ", 4)
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
# ValueError is returned if there were not enough elements to
|
||||||
|
# populate the response
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: " "received invalid response from APRS-IS"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
if callsign != self.user:
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: " "call signs differ: %s" % callsign
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
if status.startswith("unverified"):
|
||||||
|
self.logger.warning(
|
||||||
|
"socket_login: "
|
||||||
|
"invalid APRS-IS password for given call sign"
|
||||||
|
)
|
||||||
|
self.socket_close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# all validations are successful; we are connected
|
||||||
|
return True
|
||||||
|
|
||||||
|
def socket_send(self, tx_data):
|
||||||
|
"""
|
||||||
|
Generic "Send data to a socket"
|
||||||
|
"""
|
||||||
|
self.logger.debug("socket_send: init")
|
||||||
|
|
||||||
|
# Check if we are connected
|
||||||
|
if not self.sock:
|
||||||
|
self.logger.warning("socket_send: Not connected to APRS-IS")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Encode our data if we are on Python3 or later
|
||||||
|
payload = (
|
||||||
|
tx_data.encode("utf-8") if sys.version_info[0] >= 3 else tx_data
|
||||||
|
)
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
|
||||||
|
# Try to open the socket
|
||||||
|
# Send the content to APRS-IS
|
||||||
|
try:
|
||||||
|
self.sock.setblocking(True)
|
||||||
|
self.sock.settimeout(self.socket_connect_timeout)
|
||||||
|
self.sock.sendall(payload)
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
self.logger.warning("Socket Exception socket_send: %s" % str(e))
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except socket.timeout as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"Socket Timeout Exception " "socket_send: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"General Exception " "socket_send: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logger.debug("socket_send: successful")
|
||||||
|
|
||||||
|
# mandatory on several APRS-IS servers
|
||||||
|
# helps to reduce the number of errors where
|
||||||
|
# the server only returns an abbreviated message
|
||||||
|
return True
|
||||||
|
|
||||||
|
def socket_reset(self):
|
||||||
|
"""
|
||||||
|
Resets the socket's buffer
|
||||||
|
"""
|
||||||
|
self.logger.debug("socket_reset: init")
|
||||||
|
_ = self.socket_receive(0)
|
||||||
|
self.logger.debug("socket_reset: successful")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def socket_receive(self, rx_len):
|
||||||
|
"""
|
||||||
|
Generic "Receive data from a socket"
|
||||||
|
"""
|
||||||
|
self.logger.debug("socket_receive: init")
|
||||||
|
|
||||||
|
# Check if we are connected
|
||||||
|
if not self.sock:
|
||||||
|
self.logger.warning("socket_receive: not connected to APRS-IS")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# len is zero in case we intend to
|
||||||
|
# reset the socket
|
||||||
|
if rx_len > 0:
|
||||||
|
self.logger.debug("socket_receive: Receiving data from APRS-IS")
|
||||||
|
|
||||||
|
# Receive content from the socket
|
||||||
|
try:
|
||||||
|
self.sock.setblocking(False)
|
||||||
|
self.sock.settimeout(self.socket_connect_timeout)
|
||||||
|
rx_buf = self.sock.recv(rx_len)
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"Socket Exception socket_receive: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except socket.timeout as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"Socket Timeout Exception " "socket_receive: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
"General Exception " "socket_receive: %s" % str(e)
|
||||||
|
)
|
||||||
|
self.sock = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
rx_buf = (
|
||||||
|
rx_buf.decode(self.aprs_encoding)
|
||||||
|
if sys.version_info[0] >= 3 else rx_buf
|
||||||
|
)
|
||||||
|
|
||||||
|
# There will be no data in case we reset the socket
|
||||||
|
if rx_len > 0:
|
||||||
|
self.logger.debug("Received content: {}".format(rx_buf))
|
||||||
|
|
||||||
|
self.logger.debug("socket_receive: successful")
|
||||||
|
|
||||||
|
return rx_buf.rstrip()
|
||||||
|
|
||||||
|
def send(self, body, title="", notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform APRS Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.targets:
|
||||||
|
# There is no one to notify; we're done
|
||||||
|
self.logger.warning(
|
||||||
|
"There are no amateur radio call signs to notify"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# prepare payload
|
||||||
|
payload = body
|
||||||
|
|
||||||
|
# sock object is "None" if we were unable to establish a connection
|
||||||
|
# In case of errors, the error message has already been sent
|
||||||
|
# to the logger object
|
||||||
|
if not self.socket_open():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# We have established a successful connection
|
||||||
|
# to the socket server. Now send the login information
|
||||||
|
if not self.aprsis_login():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Login & authorization confirmed
|
||||||
|
# reset what is in our buffer
|
||||||
|
self.socket_reset()
|
||||||
|
|
||||||
|
# error tracking (used for function return)
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
# Create a copy of the targets list
|
||||||
|
targets = list(self.targets)
|
||||||
|
|
||||||
|
self.logger.debug("Starting Payload setup")
|
||||||
|
|
||||||
|
# Prepare the outgoing message
|
||||||
|
# Due to APRS's contraints, we need to do
|
||||||
|
# a lot of filtering before we can send
|
||||||
|
# the actual message
|
||||||
|
#
|
||||||
|
# First remove all characters from the
|
||||||
|
# payload that would break APRS
|
||||||
|
# see https://www.aprs.org/doc/APRS101.PDF pg. 71
|
||||||
|
payload = re.sub("[{}|~]+", "", payload)
|
||||||
|
|
||||||
|
payload = (
|
||||||
|
APRS_COMPILED_MAP.sub(
|
||||||
|
lambda x: APRS_BAD_CHARMAP[x.group()], payload)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finally, constrain output string to 67 characters as
|
||||||
|
# APRS messages are limited in length
|
||||||
|
payload = payload[:67]
|
||||||
|
|
||||||
|
# Our outgoing message MUST end with a CRLF so
|
||||||
|
# let's amend our payload respectively
|
||||||
|
payload = payload.rstrip("\r\n") + "\r\n"
|
||||||
|
|
||||||
|
self.logger.debug("Payload setup complete: {}".format(payload))
|
||||||
|
|
||||||
|
# send the message to our target call sign(s)
|
||||||
|
for index in range(0, len(targets)):
|
||||||
|
# prepare the output string
|
||||||
|
# Format:
|
||||||
|
# Device ID/TOCALL - our call sign - target call sign - body
|
||||||
|
buffer = "{}>{}::{:9}:{}".format(
|
||||||
|
self.user, self.device_id, targets[index], payload
|
||||||
|
)
|
||||||
|
|
||||||
|
# and send the content to the socket
|
||||||
|
# Note that there will be no response from APRS and
|
||||||
|
# that all exceptions are handled within the 'send' method
|
||||||
|
self.logger.debug("Sending APRS message: {}".format(buffer))
|
||||||
|
|
||||||
|
# send the content
|
||||||
|
if not self.socket_send(buffer):
|
||||||
|
has_error = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Finally, reset our socket buffer
|
||||||
|
# we DO NOT read from the socket as we
|
||||||
|
# would simply listen to the default APRS-IS stream
|
||||||
|
self.socket_reset()
|
||||||
|
|
||||||
|
self.logger.debug("Closing socket.")
|
||||||
|
self.socket_close()
|
||||||
|
self.logger.info(
|
||||||
|
"Sent %d/%d APRS-IS notification(s)", index + 1, len(targets))
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define any URL parameters
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if self.locale != NotifyAprs.template_args["locale"]["default"]:
|
||||||
|
# Store our locale if not default
|
||||||
|
params['locale'] = self.locale
|
||||||
|
|
||||||
|
# Extend our parameters
|
||||||
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
# Setup Authentication
|
||||||
|
auth = "{user}:{password}@".format(
|
||||||
|
user=NotifyAprs.quote(self.user, safe=""),
|
||||||
|
password=self.pprint(
|
||||||
|
self.password, privacy, mode=PrivacyMode.Secret, safe=""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return "{schema}://{auth}{targets}?{params}".format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
auth=auth,
|
||||||
|
targets="/".join(chain(
|
||||||
|
[self.pprint(x, privacy, safe="") for x in self.targets],
|
||||||
|
[self.pprint(x, privacy, safe="")
|
||||||
|
for x in self.invalid_targets],
|
||||||
|
)),
|
||||||
|
params=NotifyAprs.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""
|
||||||
|
Returns the number of targets associated with this notification
|
||||||
|
"""
|
||||||
|
targets = len(self.targets)
|
||||||
|
return targets if targets > 0 else 1
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""
|
||||||
|
Ensure we close any lingering connections
|
||||||
|
"""
|
||||||
|
self.socket_close()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
us to re-instantiate this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
if not results:
|
||||||
|
# We're done early as we couldn't load the results
|
||||||
|
return results
|
||||||
|
|
||||||
|
# All elements are targets
|
||||||
|
results["targets"] = [NotifyAprs.unquote(results["host"])]
|
||||||
|
|
||||||
|
# All entries after the hostname are additional targets
|
||||||
|
results["targets"].extend(NotifyAprs.split_path(results["fullpath"]))
|
||||||
|
|
||||||
|
# Support the 'to' variable so that we can support rooms this way too
|
||||||
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
if "to" in results["qsd"] and len(results["qsd"]["to"]):
|
||||||
|
results["targets"] += NotifyAprs.parse_list(results["qsd"]["to"])
|
||||||
|
|
||||||
|
# Set our APRS-IS server locale's key value and convert it to uppercase
|
||||||
|
if "locale" in results["qsd"] and len(results["qsd"]["locale"]):
|
||||||
|
results["locale"] = NotifyAprs.unquote(
|
||||||
|
results["qsd"]["locale"]
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
return results
|
@ -39,18 +39,19 @@ Apprise is a Python package for simplifying access to all of the different
|
|||||||
notification services that are out there. Apprise opens the door and makes
|
notification services that are out there. Apprise opens the door and makes
|
||||||
it easy to access:
|
it easy to access:
|
||||||
|
|
||||||
Apprise API, AWS SES, AWS SNS, Bark, Boxcar, Burst SMS, BulkSMS, ClickSend,
|
Apprise API, APRS, AWS SES, AWS SNS, Bark, Boxcar, Burst SMS, BulkSMS,
|
||||||
DAPNET, DingTalk, Discord, E-Mail, Emby, Faast, FCM, Flock, Google Chat,
|
ClickSend, DAPNET, DingTalk, Discord, E-Mail, Emby, Faast, FCM, Flock,
|
||||||
Gotify, Growl, Guilded, Home Assistant, IFTTT, Join, Kavenegar, KODI, Kumulos,
|
Google Chat, Gotify, Growl, Guilded, Home Assistant, IFTTT, Join, Kavenegar,
|
||||||
LaMetric, Line, MacOSX, Mailgun, Mastodon, Mattermost, Matrix, MessageBird,
|
KODI, Kumulos, LaMetric, Line, MacOSX, Mailgun, Mastodon, Mattermost,
|
||||||
Microsoft Windows, Microsoft Teams, Misskey, MQTT, MSG91, MyAndroid, Nexmo,
|
Matrix, MessageBird, Microsoft Windows, Microsoft Teams, Misskey, MQTT,
|
||||||
Nextcloud, NextcloudTalk, Notica, Notifiarr, Notifico, ntfy, Office365,
|
MSG91, MyAndroid, Nexmo, Nextcloud, NextcloudTalk, Notica, Notifiarr,
|
||||||
OneSignal, Opsgenie, PagerDuty, PagerTree, ParsePlatform, PopcornNotify,
|
Notifico, ntfy, Office365, OneSignal, Opsgenie, PagerDuty, PagerTree,
|
||||||
Prowl, Pushalot, PushBullet, Pushjet, PushMe, Pushover, PushSafer, Pushy,
|
ParsePlatform, PopcornNotify, Prowl, Pushalot, PushBullet, Pushjet, PushMe,
|
||||||
PushDeer, Reddit, Rocket.Chat, RSyslog, SendGrid, ServerChan, Signal,
|
Pushover, PushSafer, Pushy, PushDeer, Reddit, Rocket.Chat, RSyslog, SendGrid,
|
||||||
SimplePush, Sinch, Slack, SMSEagle, SMTP2Go, Spontit, SparkPost, Super Toasty,
|
ServerChan, Signal, SimplePush, Sinch, Slack, SMSEagle, SMTP2Go, Spontit,
|
||||||
Streamlabs, Stride, Synology Chat, Syslog, Techulus Push, Telegram, Threema
|
SparkPost, Super Toasty, Streamlabs, Stride, Synology Chat, Syslog,
|
||||||
Gateway, Twilio, Twitter, Twist, XBMC, Voipms, Vonage, WhatsApp, Webex Teams}
|
Techulus Push, Telegram, Threema Gateway, Twilio, Twitter, Twist, XBMC,
|
||||||
|
Voipms, Vonage, WhatsApp, Webex Teams}
|
||||||
|
|
||||||
Name: python-%{pypi_name}
|
Name: python-%{pypi_name}
|
||||||
Version: 1.6.0
|
Version: 1.6.0
|
||||||
|
355
test/test_plugin_aprs.py
Normal file
355
test/test_plugin_aprs.py
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# BSD 2-Clause License
|
||||||
|
#
|
||||||
|
# Apprise - Push Notification Library.
|
||||||
|
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
#
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
import socket
|
||||||
|
import apprise
|
||||||
|
from apprise.plugins.NotifyAprs import NotifyAprs
|
||||||
|
|
||||||
|
# Disable logging for a cleaner testing output
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('socket.create_connection')
|
||||||
|
def test_plugin_aprs_urls(mock_create_connection):
|
||||||
|
"""
|
||||||
|
NotifyAprs() Apprise URLs
|
||||||
|
|
||||||
|
"""
|
||||||
|
# A socket object
|
||||||
|
sobj = mock.Mock()
|
||||||
|
sobj.return_value = 1
|
||||||
|
sobj.getpeername.return_value = ('localhost', 1234)
|
||||||
|
sobj.socket_close.return_value = None
|
||||||
|
sobj.setblocking.return_value = True
|
||||||
|
sobj.recv.return_value = \
|
||||||
|
'ping\npong pong DF1JSL-15 verified pong'.encode('latin-1')
|
||||||
|
sobj.sendall.return_value = True
|
||||||
|
sobj.settimeout.return_value = True
|
||||||
|
|
||||||
|
# Prepare Mock
|
||||||
|
mock_create_connection.return_value = sobj
|
||||||
|
|
||||||
|
# Test invalid URLs
|
||||||
|
assert apprise.Apprise.instantiate("aprs://") is None
|
||||||
|
assert apprise.Apprise.instantiate("aprs://:@/") is None
|
||||||
|
|
||||||
|
# No call-sign specified
|
||||||
|
assert apprise.Apprise.instantiate("aprs://DF1JSL-15:12345") is None
|
||||||
|
|
||||||
|
# Garbage
|
||||||
|
assert NotifyAprs.parse_url(None) is None
|
||||||
|
|
||||||
|
# Valid call-sign but no password
|
||||||
|
assert apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:@DF1ABC") is None
|
||||||
|
assert apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15@DF1ABC") is None
|
||||||
|
# Password of -1 not supported
|
||||||
|
assert apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:-1@DF1ABC") is None
|
||||||
|
# Alpha Password not supported
|
||||||
|
assert apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:abcd@DF1ABC") is None
|
||||||
|
|
||||||
|
# Valid instances
|
||||||
|
instance = apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:12345@DF1ABC")
|
||||||
|
assert isinstance(instance, NotifyAprs)
|
||||||
|
assert instance.url(privacy=True).startswith(
|
||||||
|
'aprs://DF1JSL-15:****@D...C?')
|
||||||
|
assert instance.notify('test') is True
|
||||||
|
|
||||||
|
instance = apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:12345@DF1ABC/DF1DEF")
|
||||||
|
assert isinstance(instance, NotifyAprs)
|
||||||
|
assert instance.url(privacy=True).startswith(
|
||||||
|
'aprs://DF1JSL-15:****@D...C/D...F?')
|
||||||
|
assert instance.notify('test') is True
|
||||||
|
|
||||||
|
instance = apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:12345@DF1ABC-1/DF1ABC/DF1ABC-15")
|
||||||
|
assert isinstance(instance, NotifyAprs)
|
||||||
|
assert instance.url(privacy=True).startswith(
|
||||||
|
'aprs://DF1JSL-15:****@D...1/D...C/D...5?')
|
||||||
|
assert instance.notify('test') is True
|
||||||
|
|
||||||
|
instance = apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:12345@?to=DF1ABC,DF1DEF")
|
||||||
|
assert isinstance(instance, NotifyAprs)
|
||||||
|
assert instance.url(privacy=True).startswith(
|
||||||
|
'aprs://DF1JSL-15:****@D...C/D...F?')
|
||||||
|
assert instance.notify('test') is True
|
||||||
|
|
||||||
|
# Test Locale settings
|
||||||
|
instance = apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:12345@DF1ABC?locale=EURO")
|
||||||
|
assert isinstance(instance, NotifyAprs)
|
||||||
|
assert instance.url(privacy=True).startswith(
|
||||||
|
'aprs://DF1JSL-15:****@D...C?')
|
||||||
|
# we used the default locale, so no setting
|
||||||
|
assert 'locale=' not in instance.url(privacy=True)
|
||||||
|
assert instance.notify('test') is True
|
||||||
|
|
||||||
|
instance = apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:12345@DF1ABC?locale=NOAM")
|
||||||
|
assert isinstance(instance, NotifyAprs)
|
||||||
|
assert instance.url(privacy=True).startswith(
|
||||||
|
'aprs://DF1JSL-15:****@D...C?')
|
||||||
|
# locale is set in URL
|
||||||
|
assert 'locale=NOAM' in instance.url(privacy=True)
|
||||||
|
assert instance.notify('test') is True
|
||||||
|
|
||||||
|
# Invalid locale
|
||||||
|
assert apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:12345@DF1ABC?locale=invalid") is None
|
||||||
|
|
||||||
|
# Invalid call signs
|
||||||
|
instance = apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:12345@abcdefghi/a")
|
||||||
|
|
||||||
|
# We still instantiate
|
||||||
|
assert isinstance(instance, NotifyAprs)
|
||||||
|
|
||||||
|
# We still load our bad entries
|
||||||
|
assert instance.url(privacy=True).startswith(
|
||||||
|
"aprs://DF1JSL-15:****@A...I/A...A?")
|
||||||
|
|
||||||
|
# But with only bad entries, we have nothing to notify
|
||||||
|
assert instance.notify('test') is False
|
||||||
|
|
||||||
|
# Enforces a close
|
||||||
|
del instance
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('socket.create_connection')
|
||||||
|
def test_plugin_aprs_edge_cases(mock_create_connection):
|
||||||
|
"""
|
||||||
|
NotifyAprs() Edge Cases
|
||||||
|
"""
|
||||||
|
|
||||||
|
# A socket object
|
||||||
|
sobj = mock.Mock()
|
||||||
|
sobj.return_value = 1
|
||||||
|
sobj.getpeername.return_value = ('localhost', 1234)
|
||||||
|
sobj.socket_close.return_value = None
|
||||||
|
sobj.setblocking.return_value = True
|
||||||
|
sobj.recv.return_value = \
|
||||||
|
'ping\npong pong DF1JSL-15 verified pong'.encode('latin-1')
|
||||||
|
sobj.sendall.return_value = True
|
||||||
|
sobj.settimeout.return_value = True
|
||||||
|
|
||||||
|
# Prepare Mock
|
||||||
|
mock_create_connection.return_value = sobj
|
||||||
|
|
||||||
|
# Valid instances
|
||||||
|
instance = apprise.Apprise.instantiate(
|
||||||
|
"aprs://DF1JSL-15:12345@DF1ABC/DF1DEF")
|
||||||
|
assert isinstance(instance, NotifyAprs)
|
||||||
|
|
||||||
|
# Objects read
|
||||||
|
assert len(instance) == 2
|
||||||
|
|
||||||
|
# Bad data
|
||||||
|
sobj.recv.return_value = 'one line'.encode('latin-1')
|
||||||
|
assert instance.notify(body='body', title='title') is False
|
||||||
|
sobj.recv.return_value = '\n\n\n'.encode('latin-1')
|
||||||
|
assert instance.notify(body='body', title='title') is False
|
||||||
|
sobj.recv.return_value = ''.encode('latin-1')
|
||||||
|
assert instance.notify(body='body', title='title') is False
|
||||||
|
sobj.recv.return_value = '\ndata'.encode('latin-1')
|
||||||
|
assert instance.notify(body='body', title='title') is False
|
||||||
|
# Different Call-Sign then what we logged in as
|
||||||
|
sobj.recv.return_value = \
|
||||||
|
'ping\npong pong DF1JSL-14 verified, pong'.encode('latin-1')
|
||||||
|
assert instance.notify(body='body', title='title') is False
|
||||||
|
# Unverified
|
||||||
|
sobj.recv.return_value = \
|
||||||
|
'ping\npong pong DF1JSL-15 unverified, pong'.encode('latin-1')
|
||||||
|
assert instance.notify(body='body', title='title') is False
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test Login edge cases
|
||||||
|
#
|
||||||
|
sobj.return_value = False
|
||||||
|
assert instance.aprsis_login() is False
|
||||||
|
sobj.return_value = 1
|
||||||
|
sobj.recv.return_value = ''.encode('latin-1')
|
||||||
|
assert instance.aprsis_login() is False
|
||||||
|
sobj.recv.return_value = \
|
||||||
|
'ping\npong pong DF1JSL-15 verified pong'.encode('latin-1')
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test Socket Send Exceptions
|
||||||
|
#
|
||||||
|
sobj.sendall.return_value = None
|
||||||
|
sobj.sendall.side_effect = socket.gaierror('gaierror')
|
||||||
|
# No connection
|
||||||
|
assert instance.socket_send('data') is False
|
||||||
|
# Ensure we have a connection before calling socket_send()
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
assert instance.socket_send('data') is False
|
||||||
|
sobj.sendall.side_effect = socket.timeout('timeout')
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
assert instance.socket_send('data') is False
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
sobj.sendall.side_effect = socket.error('error')
|
||||||
|
assert instance.socket_send('data') is False
|
||||||
|
|
||||||
|
# Login is impacted by socket_send
|
||||||
|
sobj.return_value = 1
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
assert instance.aprsis_login() is False
|
||||||
|
|
||||||
|
# Return some of our
|
||||||
|
sobj.sendall.side_effect = None
|
||||||
|
sobj.sendall.return_value = True
|
||||||
|
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
sobj.close.return_value = None
|
||||||
|
sobj.close.side_effect = socket.gaierror('gaierror')
|
||||||
|
instance.socket_close()
|
||||||
|
sobj.close.side_effect = socket.timeout('timeout')
|
||||||
|
instance.socket_close()
|
||||||
|
sobj.close.side_effect = socket.error('error')
|
||||||
|
instance.socket_close()
|
||||||
|
sobj.return_value = None
|
||||||
|
instance.socket_close()
|
||||||
|
# Socket isn't open; so we can't get content
|
||||||
|
assert instance.socket_receive(100) is False
|
||||||
|
sobj.close.side_effect = None
|
||||||
|
sobj.close.return_value = None
|
||||||
|
# Double close test
|
||||||
|
instance.socket_close()
|
||||||
|
|
||||||
|
sobj.return_value = 1
|
||||||
|
mock_create_connection.return_value = None
|
||||||
|
mock_create_connection.side_effect = socket.gaierror('gaierror')
|
||||||
|
assert instance.socket_open() is False
|
||||||
|
assert instance.notify('test') is False
|
||||||
|
mock_create_connection.side_effect = socket.timeout('timeout')
|
||||||
|
assert instance.socket_open() is False
|
||||||
|
assert instance.notify('test') is False
|
||||||
|
mock_create_connection.side_effect = socket.error('error')
|
||||||
|
assert instance.socket_open() is False
|
||||||
|
assert instance.notify('test') is False
|
||||||
|
mock_create_connection.side_effect = ConnectionError('ConnectionError')
|
||||||
|
assert instance.socket_open() is False
|
||||||
|
assert instance.notify('test') is False
|
||||||
|
|
||||||
|
# Restore our good connection
|
||||||
|
mock_create_connection.return_value = sobj
|
||||||
|
mock_create_connection.side_effect = None
|
||||||
|
|
||||||
|
# Functionality has been restored
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
|
||||||
|
# Now play with getpeername
|
||||||
|
sobj.getpeername.return_value = None
|
||||||
|
sobj.getpeername.side_effect = ValueError('getpeername ValueError')
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
|
||||||
|
sobj.getpeername.return_value = ('localhost', 1234)
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
# Test different receive settings
|
||||||
|
assert instance.socket_receive(0)
|
||||||
|
assert instance.socket_receive(-1)
|
||||||
|
assert instance.socket_receive(100)
|
||||||
|
|
||||||
|
sobj.recv.side_effect = socket.gaierror('gaierror')
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
assert instance.socket_receive(100) is False
|
||||||
|
sobj.recv.side_effect = socket.timeout('timeout')
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
assert instance.socket_receive(100) is False
|
||||||
|
sobj.recv.side_effect = socket.error('error')
|
||||||
|
assert instance.socket_open() is True
|
||||||
|
assert instance.socket_receive(100) is False
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
sobj.recv.side_effect = None
|
||||||
|
sobj.recv.return_value = \
|
||||||
|
'ping\npong pong DF1JSL-15 verified pong'.encode('latin-1')
|
||||||
|
|
||||||
|
# Simulate a successful connection, but a failed notification
|
||||||
|
# To do this we need to have a login succeed, but the second call to send
|
||||||
|
# to fail
|
||||||
|
sobj.sendall.return_value = True
|
||||||
|
assert instance.notify('test') is True
|
||||||
|
|
||||||
|
sobj.sendall.return_value = None
|
||||||
|
sobj.sendall.side_effect = (True, socket.gaierror('gaierror'))
|
||||||
|
assert instance.notify('test') is False
|
||||||
|
|
||||||
|
sobj.sendall.return_value = True
|
||||||
|
sobj.sendall.side_effect = None
|
||||||
|
del sobj
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugin_aprs_config_files():
|
||||||
|
"""
|
||||||
|
NotifyAprs() Config File Cases
|
||||||
|
"""
|
||||||
|
content = """
|
||||||
|
urls:
|
||||||
|
- aprs://DF1JSL-15:12345@DF1ABC":
|
||||||
|
- locale: NOAM
|
||||||
|
|
||||||
|
- aprs://DF1JSL-15:12345@DF1ABC:
|
||||||
|
- locale: SOAM
|
||||||
|
|
||||||
|
- aprs://DF1JSL-15:12345@DF1ABC:
|
||||||
|
- locale: EURO
|
||||||
|
|
||||||
|
- aprs://DF1JSL-15:12345@DF1ABC:
|
||||||
|
- locale: ASIA
|
||||||
|
|
||||||
|
- aprs://DF1JSL-15:12345@DF1ABC:
|
||||||
|
- locale: AUNZ
|
||||||
|
|
||||||
|
- aprs://DF1JSL-15:12345@DF1ABC:
|
||||||
|
- locale: ROTA
|
||||||
|
|
||||||
|
# This will fail to load because the locale is bad
|
||||||
|
- aprs://DF1JSL-15:12345@DF1ABC:
|
||||||
|
- locale: aprs_invalid
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create ourselves a config object
|
||||||
|
ac = apprise.AppriseConfig()
|
||||||
|
assert ac.add_config(content=content) is True
|
||||||
|
|
||||||
|
aobj = apprise.Apprise()
|
||||||
|
|
||||||
|
# Add our configuration
|
||||||
|
aobj.add(ac)
|
||||||
|
|
||||||
|
assert len(ac.servers()) == 6
|
||||||
|
assert len(aobj) == 6
|
Loading…
Reference in New Issue
Block a user