mirror of
https://github.com/craigerl/aprsd.git
synced 2025-08-01 13:12:26 -04:00
Merge pull request #70 from craigerl/utils_refactor
Refactoring/Cleanup
This commit is contained in:
commit
14f77876f9
339
aprsd/client.py
339
aprsd/client.py
@ -1,26 +1,30 @@
|
|||||||
|
import abc
|
||||||
import logging
|
import logging
|
||||||
import select
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import aprslib
|
import aprslib
|
||||||
from aprslib import is_py3
|
from aprslib.exceptions import LoginError
|
||||||
from aprslib.exceptions import (
|
|
||||||
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
|
|
||||||
UnknownFormat,
|
|
||||||
)
|
|
||||||
|
|
||||||
import aprsd
|
from aprsd import trace
|
||||||
from aprsd import stats
|
from aprsd.clients import aprsis, kiss
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
TRANSPORT_APRSIS = "aprsis"
|
||||||
|
TRANSPORT_TCPKISS = "tcpkiss"
|
||||||
|
TRANSPORT_SERIALKISS = "serialkiss"
|
||||||
|
|
||||||
|
# Main must create this from the ClientFactory
|
||||||
|
# object such that it's populated with the
|
||||||
|
# Correct config
|
||||||
|
factory = None
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
"""Singleton client class that constructs the aprslib connection."""
|
"""Singleton client class that constructs the aprslib connection."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
aprs_client = None
|
_client = None
|
||||||
config = None
|
config = None
|
||||||
|
|
||||||
connected = False
|
connected = False
|
||||||
@ -38,21 +42,51 @@ class Client:
|
|||||||
if config:
|
if config:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
def new(self):
|
|
||||||
obj = super().__new__(Client)
|
|
||||||
obj.config = self.config
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self):
|
def client(self):
|
||||||
if not self.aprs_client:
|
if not self._client:
|
||||||
self.aprs_client = self.setup_connection()
|
self._client = self.setup_connection()
|
||||||
return self.aprs_client
|
return self._client
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""Call this to force a rebuild/reconnect."""
|
"""Call this to force a rebuild/reconnect."""
|
||||||
del self.aprs_client
|
del self._client
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def setup_connection(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_enabled(config):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def transport(config):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def decode_packet(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class APRSISClient(Client):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_enabled(config):
|
||||||
|
# Defaults to True if the enabled flag is non existent
|
||||||
|
return config["aprs"].get("enabled", True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transport(config):
|
||||||
|
return TRANSPORT_APRSIS
|
||||||
|
|
||||||
|
def decode_packet(self, *args, **kwargs):
|
||||||
|
"""APRS lib already decodes this."""
|
||||||
|
return args[0]
|
||||||
|
|
||||||
|
@trace.trace
|
||||||
def setup_connection(self):
|
def setup_connection(self):
|
||||||
user = self.config["aprs"]["login"]
|
user = self.config["aprs"]["login"]
|
||||||
password = self.config["aprs"]["password"]
|
password = self.config["aprs"]["password"]
|
||||||
@ -60,10 +94,11 @@ class Client:
|
|||||||
port = self.config["aprs"].get("port", 14580)
|
port = self.config["aprs"].get("port", 14580)
|
||||||
connected = False
|
connected = False
|
||||||
backoff = 1
|
backoff = 1
|
||||||
|
aprs_client = None
|
||||||
while not connected:
|
while not connected:
|
||||||
try:
|
try:
|
||||||
LOG.info("Creating aprslib client")
|
LOG.info("Creating aprslib client")
|
||||||
aprs_client = Aprsdis(user, passwd=password, host=host, port=port)
|
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
|
||||||
# Force the logging to be the same
|
# Force the logging to be the same
|
||||||
aprs_client.logger = LOG
|
aprs_client.logger = LOG
|
||||||
aprs_client.connect()
|
aprs_client.connect()
|
||||||
@ -82,200 +117,92 @@ class Client:
|
|||||||
return aprs_client
|
return aprs_client
|
||||||
|
|
||||||
|
|
||||||
class Aprsdis(aprslib.IS):
|
class KISSClient(Client):
|
||||||
"""Extend the aprslib class so we can exit properly."""
|
|
||||||
|
|
||||||
# flag to tell us to stop
|
@staticmethod
|
||||||
thread_stop = False
|
def is_enabled(config):
|
||||||
|
"""Return if tcp or serial KISS is enabled."""
|
||||||
|
if "kiss" not in config:
|
||||||
|
return False
|
||||||
|
|
||||||
# timeout in seconds
|
if config.get("kiss.serial.enabled", default=False):
|
||||||
select_timeout = 1
|
return True
|
||||||
|
|
||||||
def stop(self):
|
if config.get("kiss.tcp.enabled", default=False):
|
||||||
self.thread_stop = True
|
return True
|
||||||
LOG.info("Shutdown Aprsdis client.")
|
|
||||||
|
|
||||||
def send(self, msg):
|
@staticmethod
|
||||||
"""Send an APRS Message object."""
|
def transport(config):
|
||||||
line = str(msg)
|
if config.get("kiss.serial.enabled", default=False):
|
||||||
self.sendall(line)
|
return TRANSPORT_SERIALKISS
|
||||||
|
|
||||||
def _socket_readlines(self, blocking=False):
|
if config.get("kiss.tcp.enabled", default=False):
|
||||||
"""
|
return TRANSPORT_TCPKISS
|
||||||
Generator for complete lines, received from the server
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.sock.setblocking(0)
|
|
||||||
except OSError as e:
|
|
||||||
self.logger.error(f"socket error when setblocking(0): {str(e)}")
|
|
||||||
raise aprslib.ConnectionDrop("connection dropped")
|
|
||||||
|
|
||||||
while not self.thread_stop:
|
def decode_packet(self, *args, **kwargs):
|
||||||
short_buf = b""
|
"""We get a frame, which has to be decoded."""
|
||||||
newline = b"\r\n"
|
frame = kwargs["frame"]
|
||||||
|
LOG.debug(f"Got an APRS Frame '{frame}'")
|
||||||
|
# try and nuke the * from the fromcall sign.
|
||||||
|
frame.header._source._ch = False
|
||||||
|
payload = str(frame.payload.decode())
|
||||||
|
msg = f"{str(frame.header)}:{payload}"
|
||||||
|
# msg = frame.tnc2
|
||||||
|
LOG.debug(f"Decoding {msg}")
|
||||||
|
|
||||||
# set a select timeout, so we get a chance to exit
|
packet = aprslib.parse(msg)
|
||||||
# when user hits CTRL-C
|
return packet
|
||||||
readable, writable, exceptional = select.select(
|
|
||||||
[self.sock],
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
self.select_timeout,
|
|
||||||
)
|
|
||||||
if not readable:
|
|
||||||
if not blocking:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
@trace.trace
|
||||||
short_buf = self.sock.recv(4096)
|
def setup_connection(self):
|
||||||
|
ax25client = kiss.Aioax25Client(self.config)
|
||||||
# sock.recv returns empty if the connection drops
|
return ax25client
|
||||||
if not short_buf:
|
|
||||||
if not blocking:
|
|
||||||
# We could just not be blocking, so empty is expected
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.logger.error("socket.recv(): returned empty")
|
|
||||||
raise aprslib.ConnectionDrop("connection dropped")
|
|
||||||
except OSError as e:
|
|
||||||
# self.logger.error("socket error on recv(): %s" % str(e))
|
|
||||||
if "Resource temporarily unavailable" in str(e):
|
|
||||||
if not blocking:
|
|
||||||
if len(self.buf) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
self.buf += short_buf
|
|
||||||
|
|
||||||
while newline in self.buf:
|
|
||||||
line, self.buf = self.buf.split(newline, 1)
|
|
||||||
|
|
||||||
yield line
|
|
||||||
|
|
||||||
def _send_login(self):
|
|
||||||
"""
|
|
||||||
Sends login string to server
|
|
||||||
"""
|
|
||||||
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
|
|
||||||
login_str = login_str.format(
|
|
||||||
self.callsign,
|
|
||||||
self.passwd,
|
|
||||||
(" filter " + self.filter) if self.filter != "" else "",
|
|
||||||
aprsd.__version__,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.logger.info("Sending login information")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._sendall(login_str)
|
|
||||||
self.sock.settimeout(5)
|
|
||||||
test = self.sock.recv(len(login_str) + 100)
|
|
||||||
if is_py3:
|
|
||||||
test = test.decode("latin-1")
|
|
||||||
test = test.rstrip()
|
|
||||||
|
|
||||||
self.logger.debug("Server: %s", test)
|
|
||||||
|
|
||||||
a, b, callsign, status, e = test.split(" ", 4)
|
|
||||||
s = e.split(",")
|
|
||||||
if len(s):
|
|
||||||
server_string = s[0].replace("server ", "")
|
|
||||||
else:
|
|
||||||
server_string = e.replace("server ", "")
|
|
||||||
|
|
||||||
self.logger.info(f"Connected to {server_string}")
|
|
||||||
self.server_string = server_string
|
|
||||||
stats.APRSDStats().set_aprsis_server(server_string)
|
|
||||||
|
|
||||||
if callsign == "":
|
|
||||||
raise LoginError("Server responded with empty callsign???")
|
|
||||||
if callsign != self.callsign:
|
|
||||||
raise LoginError(f"Server: {test}")
|
|
||||||
if status != "verified," and self.passwd != "-1":
|
|
||||||
raise LoginError("Password is incorrect")
|
|
||||||
|
|
||||||
if self.passwd == "-1":
|
|
||||||
self.logger.info("Login successful (receive only)")
|
|
||||||
else:
|
|
||||||
self.logger.info("Login successful")
|
|
||||||
|
|
||||||
except LoginError as e:
|
|
||||||
self.logger.error(str(e))
|
|
||||||
self.close()
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
self.close()
|
|
||||||
self.logger.error(f"Failed to login '{e}'")
|
|
||||||
raise LoginError("Failed to login")
|
|
||||||
|
|
||||||
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
|
||||||
"""
|
|
||||||
When a position sentence is received, it will be passed to the callback function
|
|
||||||
|
|
||||||
blocking: if true (default), runs forever, otherwise will return after one sentence
|
|
||||||
You can still exit the loop, by raising StopIteration in the callback function
|
|
||||||
|
|
||||||
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
|
|
||||||
if false (default), consumer will return
|
|
||||||
|
|
||||||
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not self._connected:
|
|
||||||
raise ConnectionError("not connected to a server")
|
|
||||||
|
|
||||||
line = b""
|
|
||||||
|
|
||||||
while True and not self.thread_stop:
|
|
||||||
try:
|
|
||||||
for line in self._socket_readlines(blocking):
|
|
||||||
if line[0:1] != b"#":
|
|
||||||
if raw:
|
|
||||||
callback(line)
|
|
||||||
else:
|
|
||||||
callback(self._parse(line))
|
|
||||||
else:
|
|
||||||
self.logger.debug("Server: %s", line.decode("utf8"))
|
|
||||||
stats.APRSDStats().set_aprsis_keepalive()
|
|
||||||
except ParseError as exp:
|
|
||||||
self.logger.log(
|
|
||||||
11,
|
|
||||||
"%s\n Packet: %s",
|
|
||||||
exp,
|
|
||||||
exp.packet,
|
|
||||||
)
|
|
||||||
except UnknownFormat as exp:
|
|
||||||
self.logger.log(
|
|
||||||
9,
|
|
||||||
"%s\n Packet: %s",
|
|
||||||
exp,
|
|
||||||
exp.packet,
|
|
||||||
)
|
|
||||||
except LoginError as exp:
|
|
||||||
self.logger.error("%s: %s", exp.__class__.__name__, exp)
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
|
||||||
raise
|
|
||||||
except (ConnectionDrop, ConnectionError):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
if not immortal:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
self.connect(blocking=blocking)
|
|
||||||
continue
|
|
||||||
except GenericError:
|
|
||||||
pass
|
|
||||||
except StopIteration:
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
self.logger.error("APRS Packet: %s", line)
|
|
||||||
raise
|
|
||||||
|
|
||||||
if not blocking:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def get_client():
|
class ClientFactory:
|
||||||
cl = Client()
|
_instance = None
|
||||||
return cl.client
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""This magic turns this into a singleton."""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
# Put any initialization here.
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self._builders = {}
|
||||||
|
|
||||||
|
def register(self, key, builder):
|
||||||
|
self._builders[key] = builder
|
||||||
|
|
||||||
|
def create(self, key=None):
|
||||||
|
if not key:
|
||||||
|
if APRSISClient.is_enabled(self.config):
|
||||||
|
key = TRANSPORT_APRSIS
|
||||||
|
elif KISSClient.is_enabled(self.config):
|
||||||
|
key = KISSClient.transport(self.config)
|
||||||
|
|
||||||
|
LOG.debug(f"GET client {key}")
|
||||||
|
builder = self._builders.get(key)
|
||||||
|
if not builder:
|
||||||
|
raise ValueError(key)
|
||||||
|
return builder(self.config)
|
||||||
|
|
||||||
|
def is_client_enabled(self):
|
||||||
|
"""Make sure at least one client is enabled."""
|
||||||
|
enabled = False
|
||||||
|
for key in self._builders.keys():
|
||||||
|
enabled |= self._builders[key].is_enabled(self.config)
|
||||||
|
|
||||||
|
return enabled
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def setup(config):
|
||||||
|
"""Create and register all possible client objects."""
|
||||||
|
global factory
|
||||||
|
|
||||||
|
factory = ClientFactory(config)
|
||||||
|
factory.register(TRANSPORT_APRSIS, APRSISClient)
|
||||||
|
factory.register(TRANSPORT_TCPKISS, KISSClient)
|
||||||
|
factory.register(TRANSPORT_SERIALKISS, KISSClient)
|
||||||
|
209
aprsd/clients/aprsis.py
Normal file
209
aprsd/clients/aprsis.py
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import logging
|
||||||
|
import select
|
||||||
|
|
||||||
|
import aprslib
|
||||||
|
from aprslib import is_py3
|
||||||
|
from aprslib.exceptions import (
|
||||||
|
ConnectionDrop, ConnectionError, GenericError, LoginError, ParseError,
|
||||||
|
UnknownFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
import aprsd
|
||||||
|
from aprsd import stats
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class Aprsdis(aprslib.IS):
|
||||||
|
"""Extend the aprslib class so we can exit properly."""
|
||||||
|
|
||||||
|
# flag to tell us to stop
|
||||||
|
thread_stop = False
|
||||||
|
|
||||||
|
# timeout in seconds
|
||||||
|
select_timeout = 1
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.thread_stop = True
|
||||||
|
LOG.info("Shutdown Aprsdis client.")
|
||||||
|
|
||||||
|
def send(self, msg):
|
||||||
|
"""Send an APRS Message object."""
|
||||||
|
line = str(msg)
|
||||||
|
self.sendall(line)
|
||||||
|
|
||||||
|
def _socket_readlines(self, blocking=False):
|
||||||
|
"""
|
||||||
|
Generator for complete lines, received from the server
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.sock.setblocking(0)
|
||||||
|
except OSError as e:
|
||||||
|
self.logger.error(f"socket error when setblocking(0): {str(e)}")
|
||||||
|
raise aprslib.ConnectionDrop("connection dropped")
|
||||||
|
|
||||||
|
while not self.thread_stop:
|
||||||
|
short_buf = b""
|
||||||
|
newline = b"\r\n"
|
||||||
|
|
||||||
|
# set a select timeout, so we get a chance to exit
|
||||||
|
# when user hits CTRL-C
|
||||||
|
readable, writable, exceptional = select.select(
|
||||||
|
[self.sock],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
self.select_timeout,
|
||||||
|
)
|
||||||
|
if not readable:
|
||||||
|
if not blocking:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
short_buf = self.sock.recv(4096)
|
||||||
|
|
||||||
|
# sock.recv returns empty if the connection drops
|
||||||
|
if not short_buf:
|
||||||
|
if not blocking:
|
||||||
|
# We could just not be blocking, so empty is expected
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
self.logger.error("socket.recv(): returned empty")
|
||||||
|
raise aprslib.ConnectionDrop("connection dropped")
|
||||||
|
except OSError as e:
|
||||||
|
# self.logger.error("socket error on recv(): %s" % str(e))
|
||||||
|
if "Resource temporarily unavailable" in str(e):
|
||||||
|
if not blocking:
|
||||||
|
if len(self.buf) == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.buf += short_buf
|
||||||
|
|
||||||
|
while newline in self.buf:
|
||||||
|
line, self.buf = self.buf.split(newline, 1)
|
||||||
|
|
||||||
|
yield line
|
||||||
|
|
||||||
|
def _send_login(self):
|
||||||
|
"""
|
||||||
|
Sends login string to server
|
||||||
|
"""
|
||||||
|
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
|
||||||
|
login_str = login_str.format(
|
||||||
|
self.callsign,
|
||||||
|
self.passwd,
|
||||||
|
(" filter " + self.filter) if self.filter != "" else "",
|
||||||
|
aprsd.__version__,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.logger.info("Sending login information")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._sendall(login_str)
|
||||||
|
self.sock.settimeout(5)
|
||||||
|
test = self.sock.recv(len(login_str) + 100)
|
||||||
|
if is_py3:
|
||||||
|
test = test.decode("latin-1")
|
||||||
|
test = test.rstrip()
|
||||||
|
|
||||||
|
self.logger.debug("Server: %s", test)
|
||||||
|
|
||||||
|
a, b, callsign, status, e = test.split(" ", 4)
|
||||||
|
s = e.split(",")
|
||||||
|
if len(s):
|
||||||
|
server_string = s[0].replace("server ", "")
|
||||||
|
else:
|
||||||
|
server_string = e.replace("server ", "")
|
||||||
|
|
||||||
|
self.logger.info(f"Connected to {server_string}")
|
||||||
|
self.server_string = server_string
|
||||||
|
stats.APRSDStats().set_aprsis_server(server_string)
|
||||||
|
|
||||||
|
if callsign == "":
|
||||||
|
raise LoginError("Server responded with empty callsign???")
|
||||||
|
if callsign != self.callsign:
|
||||||
|
raise LoginError(f"Server: {test}")
|
||||||
|
if status != "verified," and self.passwd != "-1":
|
||||||
|
raise LoginError("Password is incorrect")
|
||||||
|
|
||||||
|
if self.passwd == "-1":
|
||||||
|
self.logger.info("Login successful (receive only)")
|
||||||
|
else:
|
||||||
|
self.logger.info("Login successful")
|
||||||
|
|
||||||
|
except LoginError as e:
|
||||||
|
self.logger.error(str(e))
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
self.close()
|
||||||
|
self.logger.error(f"Failed to login '{e}'")
|
||||||
|
raise LoginError("Failed to login")
|
||||||
|
|
||||||
|
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||||
|
"""
|
||||||
|
When a position sentence is received, it will be passed to the callback function
|
||||||
|
|
||||||
|
blocking: if true (default), runs forever, otherwise will return after one sentence
|
||||||
|
You can still exit the loop, by raising StopIteration in the callback function
|
||||||
|
|
||||||
|
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
|
||||||
|
if false (default), consumer will return
|
||||||
|
|
||||||
|
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self._connected:
|
||||||
|
raise ConnectionError("not connected to a server")
|
||||||
|
|
||||||
|
line = b""
|
||||||
|
|
||||||
|
while True and not self.thread_stop:
|
||||||
|
try:
|
||||||
|
for line in self._socket_readlines(blocking):
|
||||||
|
if line[0:1] != b"#":
|
||||||
|
if raw:
|
||||||
|
callback(line)
|
||||||
|
else:
|
||||||
|
callback(self._parse(line))
|
||||||
|
else:
|
||||||
|
self.logger.debug("Server: %s", line.decode("utf8"))
|
||||||
|
stats.APRSDStats().set_aprsis_keepalive()
|
||||||
|
except ParseError as exp:
|
||||||
|
self.logger.log(
|
||||||
|
11,
|
||||||
|
"%s\n Packet: %s",
|
||||||
|
exp,
|
||||||
|
exp.packet,
|
||||||
|
)
|
||||||
|
except UnknownFormat as exp:
|
||||||
|
self.logger.log(
|
||||||
|
9,
|
||||||
|
"%s\n Packet: %s",
|
||||||
|
exp,
|
||||||
|
exp.packet,
|
||||||
|
)
|
||||||
|
except LoginError as exp:
|
||||||
|
self.logger.error("%s: %s", exp.__class__.__name__, exp)
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
raise
|
||||||
|
except (ConnectionDrop, ConnectionError):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
if not immortal:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.connect(blocking=blocking)
|
||||||
|
continue
|
||||||
|
except GenericError:
|
||||||
|
pass
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
self.logger.error("APRS Packet: %s", line)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not blocking:
|
||||||
|
break
|
@ -5,83 +5,20 @@ from aioax25 import interface
|
|||||||
from aioax25 import kiss as kiss
|
from aioax25 import kiss as kiss
|
||||||
from aioax25.aprs import APRSInterface
|
from aioax25.aprs import APRSInterface
|
||||||
|
|
||||||
from aprsd import trace
|
|
||||||
|
|
||||||
|
|
||||||
TRANSPORT_TCPKISS = "tcpkiss"
|
|
||||||
TRANSPORT_SERIALKISS = "serialkiss"
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class KISSClient:
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
config = None
|
|
||||||
ax25client = None
|
|
||||||
loop = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
"""Singleton for this class."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
# initialize shit here
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self, config=None):
|
|
||||||
if config:
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def kiss_enabled(config):
|
|
||||||
"""Return if tcp or serial KISS is enabled."""
|
|
||||||
if "kiss" not in config:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if "serial" in config["kiss"]:
|
|
||||||
if config["kiss"]["serial"].get("enabled", False):
|
|
||||||
return True
|
|
||||||
|
|
||||||
if "tcp" in config["kiss"]:
|
|
||||||
if config["kiss"]["tcp"].get("enabled", False):
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def transport(config):
|
|
||||||
if "serial" in config["kiss"]:
|
|
||||||
if config["kiss"]["serial"].get("enabled", False):
|
|
||||||
return TRANSPORT_SERIALKISS
|
|
||||||
|
|
||||||
if "tcp" in config["kiss"]:
|
|
||||||
if config["kiss"]["tcp"].get("enabled", False):
|
|
||||||
return TRANSPORT_TCPKISS
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client(self):
|
|
||||||
if not self.ax25client:
|
|
||||||
self.ax25client = self.setup_connection()
|
|
||||||
return self.ax25client
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""Call this to fore a rebuild/reconnect."""
|
|
||||||
self.ax25client.stop()
|
|
||||||
del self.ax25client
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def setup_connection(self):
|
|
||||||
ax25client = Aioax25Client(self.config)
|
|
||||||
LOG.debug("Complete")
|
|
||||||
return ax25client
|
|
||||||
|
|
||||||
|
|
||||||
class Aioax25Client:
|
class Aioax25Client:
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
self.loop = asyncio.get_event_loop()
|
||||||
self.setup()
|
self.setup()
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
# we can be TCP kiss or Serial kiss
|
# we can be TCP kiss or Serial kiss
|
||||||
|
|
||||||
self.loop = asyncio.get_event_loop()
|
|
||||||
if "serial" in self.config["kiss"] and self.config["kiss"]["serial"].get(
|
if "serial" in self.config["kiss"] and self.config["kiss"]["serial"].get(
|
||||||
"enabled",
|
"enabled",
|
||||||
False,
|
False,
|
||||||
@ -131,10 +68,20 @@ class Aioax25Client:
|
|||||||
self.kissdev._close()
|
self.kissdev._close()
|
||||||
self.loop.stop()
|
self.loop.stop()
|
||||||
|
|
||||||
def consumer(self, callback, callsign=None):
|
def set_filter(self, filter):
|
||||||
if not callsign:
|
# This does nothing right now.
|
||||||
callsign = self.config["ham"]["callsign"]
|
pass
|
||||||
self.aprsint.bind(callback=callback, callsign="WB4BOR", ssid=12, regex=False)
|
|
||||||
|
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||||
|
callsign = self.config["kiss"]["callsign"]
|
||||||
|
call = callsign.split("-")
|
||||||
|
if len(call) > 1:
|
||||||
|
callsign = call[0]
|
||||||
|
ssid = int(call[1])
|
||||||
|
else:
|
||||||
|
ssid = 0
|
||||||
|
self.aprsint.bind(callback=callback, callsign=callsign, ssid=ssid, regex=False)
|
||||||
|
self.loop.run_forever()
|
||||||
|
|
||||||
def send(self, msg):
|
def send(self, msg):
|
||||||
"""Send an APRS Message object."""
|
"""Send an APRS Message object."""
|
||||||
@ -145,8 +92,3 @@ class Aioax25Client:
|
|||||||
path=["WIDE1-1", "WIDE2-1"],
|
path=["WIDE1-1", "WIDE2-1"],
|
||||||
oneshot=True,
|
oneshot=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_client():
|
|
||||||
cl = KISSClient()
|
|
||||||
return cl.client
|
|
389
aprsd/config.py
Normal file
389
aprsd/config.py
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
import collections
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import click
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from aprsd import utils
|
||||||
|
|
||||||
|
|
||||||
|
LOG_LEVELS = {
|
||||||
|
"CRITICAL": logging.CRITICAL,
|
||||||
|
"ERROR": logging.ERROR,
|
||||||
|
"WARNING": logging.WARNING,
|
||||||
|
"INFO": logging.INFO,
|
||||||
|
"DEBUG": logging.DEBUG,
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
||||||
|
DEFAULT_LOG_FORMAT = (
|
||||||
|
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
||||||
|
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||||
|
)
|
||||||
|
|
||||||
|
QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]"
|
||||||
|
QUEUE_LOG_FORMAT = (
|
||||||
|
"%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
||||||
|
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||||
|
)
|
||||||
|
|
||||||
|
CORE_MESSAGE_PLUGINS = [
|
||||||
|
"aprsd.plugins.email.EmailPlugin",
|
||||||
|
"aprsd.plugins.fortune.FortunePlugin",
|
||||||
|
"aprsd.plugins.location.LocationPlugin",
|
||||||
|
"aprsd.plugins.ping.PingPlugin",
|
||||||
|
"aprsd.plugins.query.QueryPlugin",
|
||||||
|
"aprsd.plugins.stock.StockPlugin",
|
||||||
|
"aprsd.plugins.time.TimePlugin",
|
||||||
|
"aprsd.plugins.weather.USWeatherPlugin",
|
||||||
|
"aprsd.plugins.version.VersionPlugin",
|
||||||
|
]
|
||||||
|
|
||||||
|
CORE_NOTIFY_PLUGINS = [
|
||||||
|
"aprsd.plugins.notify.NotifySeenPlugin",
|
||||||
|
]
|
||||||
|
|
||||||
|
# an example of what should be in the ~/.aprsd/config.yml
|
||||||
|
DEFAULT_CONFIG_DICT = {
|
||||||
|
"ham": {"callsign": "NOCALL"},
|
||||||
|
"aprs": {
|
||||||
|
"enabled": True,
|
||||||
|
"login": "CALLSIGN",
|
||||||
|
"password": "00000",
|
||||||
|
"host": "rotate.aprs2.net",
|
||||||
|
"port": 14580,
|
||||||
|
},
|
||||||
|
"kiss": {
|
||||||
|
"tcp": {
|
||||||
|
"enabled": False,
|
||||||
|
"host": "direwolf.ip.address",
|
||||||
|
"port": "8001",
|
||||||
|
},
|
||||||
|
"serial": {
|
||||||
|
"enabled": False,
|
||||||
|
"device": "/dev/ttyS0",
|
||||||
|
"baudrate": 9600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"aprsd": {
|
||||||
|
"logfile": "/tmp/aprsd.log",
|
||||||
|
"logformat": DEFAULT_LOG_FORMAT,
|
||||||
|
"dateformat": DEFAULT_DATE_FORMAT,
|
||||||
|
"trace": False,
|
||||||
|
"enabled_plugins": CORE_MESSAGE_PLUGINS,
|
||||||
|
"units": "imperial",
|
||||||
|
"watch_list": {
|
||||||
|
"enabled": False,
|
||||||
|
# Who gets the alert?
|
||||||
|
"alert_callsign": "NOCALL",
|
||||||
|
# 43200 is 12 hours
|
||||||
|
"alert_time_seconds": 43200,
|
||||||
|
# How many packets to save in a ring Buffer
|
||||||
|
# for a particular callsign
|
||||||
|
"packet_keep_count": 10,
|
||||||
|
"callsigns": [],
|
||||||
|
"enabled_plugins": CORE_NOTIFY_PLUGINS,
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"enabled": True,
|
||||||
|
"logging_enabled": True,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8001,
|
||||||
|
"users": {
|
||||||
|
"admin": "password-here",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"enabled": True,
|
||||||
|
"shortcuts": {
|
||||||
|
"aa": "5551239999@vtext.com",
|
||||||
|
"cl": "craiglamparter@somedomain.org",
|
||||||
|
"wb": "555309@vtext.com",
|
||||||
|
},
|
||||||
|
"smtp": {
|
||||||
|
"login": "SMTP_USERNAME",
|
||||||
|
"password": "SMTP_PASSWORD",
|
||||||
|
"host": "smtp.gmail.com",
|
||||||
|
"port": 465,
|
||||||
|
"use_ssl": False,
|
||||||
|
"debug": False,
|
||||||
|
},
|
||||||
|
"imap": {
|
||||||
|
"login": "IMAP_USERNAME",
|
||||||
|
"password": "IMAP_PASSWORD",
|
||||||
|
"host": "imap.gmail.com",
|
||||||
|
"port": 993,
|
||||||
|
"use_ssl": True,
|
||||||
|
"debug": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"aprs.fi": {"apiKey": "APIKEYVALUE"},
|
||||||
|
"openweathermap": {"apiKey": "APIKEYVALUE"},
|
||||||
|
"opencagedata": {"apiKey": "APIKEYVALUE"},
|
||||||
|
"avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
home = str(Path.home())
|
||||||
|
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
|
||||||
|
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
|
||||||
|
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml"
|
||||||
|
|
||||||
|
|
||||||
|
class Config(collections.UserDict):
|
||||||
|
def _get(self, d, keys, default=None):
|
||||||
|
"""
|
||||||
|
Example:
|
||||||
|
d = {'meta': {'status': 'OK', 'status_code': 200}}
|
||||||
|
deep_get(d, ['meta', 'status_code']) # => 200
|
||||||
|
deep_get(d, ['garbage', 'status_code']) # => None
|
||||||
|
deep_get(d, ['meta', 'garbage'], default='-') # => '-'
|
||||||
|
|
||||||
|
"""
|
||||||
|
if type(keys) is str and "." in keys:
|
||||||
|
keys = keys.split(".")
|
||||||
|
|
||||||
|
assert type(keys) is list
|
||||||
|
if d is None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
if not keys:
|
||||||
|
return d
|
||||||
|
|
||||||
|
if type(d) is str:
|
||||||
|
return default
|
||||||
|
|
||||||
|
return self._get(d.get(keys[0]), keys[1:], default)
|
||||||
|
|
||||||
|
def get(self, path, default=None):
|
||||||
|
return self._get(self.data, path, default=default)
|
||||||
|
|
||||||
|
def exists(self, path):
|
||||||
|
"""See if a conf value exists."""
|
||||||
|
test = "-3.14TEST41.3-"
|
||||||
|
return (self.get(path, default=test) != test)
|
||||||
|
|
||||||
|
def check_option(self, path, default_fail=None):
|
||||||
|
"""Make sure the config option doesn't have default value."""
|
||||||
|
if not self.exists(path):
|
||||||
|
raise Exception(
|
||||||
|
"Option '{}' was not in config file".format(
|
||||||
|
path,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val = self.get(path)
|
||||||
|
if val == default_fail:
|
||||||
|
# We have to fail and bail if the user hasn't edited
|
||||||
|
# this config option.
|
||||||
|
raise Exception(
|
||||||
|
"Config file needs to be changed from provided"
|
||||||
|
" defaults for '{}'".format(
|
||||||
|
path,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_config_comments(raw_yaml):
|
||||||
|
end_idx = utils.end_substr(raw_yaml, "aprs:")
|
||||||
|
if end_idx != -1:
|
||||||
|
# lets insert a comment
|
||||||
|
raw_yaml = utils.insert_str(
|
||||||
|
raw_yaml,
|
||||||
|
"\n # Set enabled to False if there is no internet connectivity."
|
||||||
|
"\n # This is useful for a direwolf KISS aprs connection only. "
|
||||||
|
"\n"
|
||||||
|
"\n # Get the passcode for your callsign here: "
|
||||||
|
"\n # https://apps.magicbug.co.uk/passcode",
|
||||||
|
end_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
end_idx = utils.end_substr(raw_yaml, "aprs.fi:")
|
||||||
|
if end_idx != -1:
|
||||||
|
# lets insert a comment
|
||||||
|
raw_yaml = utils.insert_str(
|
||||||
|
raw_yaml,
|
||||||
|
"\n # Get the apiKey from your aprs.fi account here: "
|
||||||
|
"\n # http://aprs.fi/account",
|
||||||
|
end_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
end_idx = utils.end_substr(raw_yaml, "opencagedata:")
|
||||||
|
if end_idx != -1:
|
||||||
|
# lets insert a comment
|
||||||
|
raw_yaml = utils.insert_str(
|
||||||
|
raw_yaml,
|
||||||
|
"\n # (Optional for TimeOpenCageDataPlugin) "
|
||||||
|
"\n # Get the apiKey from your opencagedata account here: "
|
||||||
|
"\n # https://opencagedata.com/dashboard#api-keys",
|
||||||
|
end_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
end_idx = utils.end_substr(raw_yaml, "openweathermap:")
|
||||||
|
if end_idx != -1:
|
||||||
|
# lets insert a comment
|
||||||
|
raw_yaml = utils.insert_str(
|
||||||
|
raw_yaml,
|
||||||
|
"\n # (Optional for OWMWeatherPlugin) "
|
||||||
|
"\n # Get the apiKey from your "
|
||||||
|
"\n # openweathermap account here: "
|
||||||
|
"\n # https://home.openweathermap.org/api_keys",
|
||||||
|
end_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
end_idx = utils.end_substr(raw_yaml, "avwx:")
|
||||||
|
if end_idx != -1:
|
||||||
|
# lets insert a comment
|
||||||
|
raw_yaml = utils.insert_str(
|
||||||
|
raw_yaml,
|
||||||
|
"\n # (Optional for AVWXWeatherPlugin) "
|
||||||
|
"\n # Use hosted avwx-api here: https://avwx.rest "
|
||||||
|
"\n # or deploy your own from here: "
|
||||||
|
"\n # https://github.com/avwx-rest/avwx-api",
|
||||||
|
end_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
return raw_yaml
|
||||||
|
|
||||||
|
|
||||||
|
def dump_default_cfg():
|
||||||
|
return add_config_comments(
|
||||||
|
yaml.dump(
|
||||||
|
DEFAULT_CONFIG_DICT,
|
||||||
|
indent=4,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_default_config():
|
||||||
|
"""Create a default config file."""
|
||||||
|
# make sure the directory location exists
|
||||||
|
config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE)
|
||||||
|
config_dir = os.path.dirname(config_file_expanded)
|
||||||
|
if not os.path.exists(config_dir):
|
||||||
|
click.echo(f"Config dir '{config_dir}' doesn't exist, creating.")
|
||||||
|
utils.mkdir_p(config_dir)
|
||||||
|
with open(config_file_expanded, "w+") as cf:
|
||||||
|
cf.write(dump_default_cfg())
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(config_file):
|
||||||
|
"""This tries to read the yaml config from <config_file>."""
|
||||||
|
config_file_expanded = os.path.expanduser(config_file)
|
||||||
|
if os.path.exists(config_file_expanded):
|
||||||
|
with open(config_file_expanded) as stream:
|
||||||
|
config = yaml.load(stream, Loader=yaml.FullLoader)
|
||||||
|
return Config(config)
|
||||||
|
else:
|
||||||
|
if config_file == DEFAULT_CONFIG_FILE:
|
||||||
|
click.echo(
|
||||||
|
f"{config_file_expanded} is missing, creating config file",
|
||||||
|
)
|
||||||
|
create_default_config()
|
||||||
|
msg = (
|
||||||
|
"Default config file created at {}. Please edit with your "
|
||||||
|
"settings.".format(config_file)
|
||||||
|
)
|
||||||
|
click.echo(msg)
|
||||||
|
else:
|
||||||
|
# The user provided a config file path different from the
|
||||||
|
# Default, so we won't try and create it, just bitch and bail.
|
||||||
|
msg = f"Custom config file '{config_file}' is missing."
|
||||||
|
click.echo(msg)
|
||||||
|
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
|
||||||
|
# This method tries to parse the config yaml file
|
||||||
|
# and consume the settings.
|
||||||
|
# If the required params don't exist,
|
||||||
|
# it will look in the environment
|
||||||
|
def parse_config(config_file):
|
||||||
|
config = get_config(config_file)
|
||||||
|
|
||||||
|
def fail(msg):
|
||||||
|
click.echo(msg)
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
def check_option(config, path, default_fail=None):
|
||||||
|
try:
|
||||||
|
config.check_option(path, default_fail=default_fail)
|
||||||
|
except Exception as ex:
|
||||||
|
fail(repr(ex))
|
||||||
|
else:
|
||||||
|
return config
|
||||||
|
|
||||||
|
# special check here to make sure user has edited the config file
|
||||||
|
# and changed the ham callsign
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
"ham.callsign",
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
|
||||||
|
)
|
||||||
|
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
["services", "aprs.fi", "apiKey"],
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"],
|
||||||
|
)
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
"aprs.login",
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"],
|
||||||
|
)
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
["aprs", "password"],
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure they change the admin password
|
||||||
|
if config.get("aprsd.web.enabled") is True:
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
["aprsd", "web", "users", "admin"],
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if config.get("aprsd.watch_list.enabled") is True:
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
["aprsd", "watch_list", "alert_callsign"],
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if config.get("aprsd.email.enabled") is True:
|
||||||
|
# Check IMAP server settings
|
||||||
|
check_option(config, ["aprsd", "email", "imap", "host"])
|
||||||
|
check_option(config, ["aprsd", "email", "imap", "port"])
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
["aprsd", "email", "imap", "login"],
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
|
||||||
|
)
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
["aprsd", "email", "imap", "password"],
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check SMTP server settings
|
||||||
|
check_option(config, ["aprsd", "email", "smtp", "host"])
|
||||||
|
check_option(config, ["aprsd", "email", "smtp", "port"])
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
["aprsd", "email", "smtp", "login"],
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
|
||||||
|
)
|
||||||
|
check_option(
|
||||||
|
config,
|
||||||
|
["aprsd", "email", "smtp", "password"],
|
||||||
|
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
12
aprsd/dev.py
12
aprsd/dev.py
@ -14,7 +14,9 @@ import click_completion
|
|||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import client, plugin, utils
|
from aprsd import client
|
||||||
|
from aprsd import config as aprsd_config
|
||||||
|
from aprsd import plugin
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
@ -156,7 +158,7 @@ def setup_logging(config, loglevel, quiet):
|
|||||||
"--config",
|
"--config",
|
||||||
"config_file",
|
"config_file",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
default=utils.DEFAULT_CONFIG_FILE,
|
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||||
help="The aprsd config file to use for options.",
|
help="The aprsd config file to use for options.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@ -178,7 +180,7 @@ def test_plugin(
|
|||||||
):
|
):
|
||||||
"""APRSD Plugin test app."""
|
"""APRSD Plugin test app."""
|
||||||
|
|
||||||
config = utils.parse_config(config_file)
|
config = aprsd_config.parse_config(config_file)
|
||||||
|
|
||||||
setup_logging(config, loglevel, False)
|
setup_logging(config, loglevel, False)
|
||||||
LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}")
|
LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}")
|
||||||
@ -188,7 +190,9 @@ def test_plugin(
|
|||||||
client.Client(config)
|
client.Client(config)
|
||||||
|
|
||||||
pm = plugin.PluginManager(config)
|
pm = plugin.PluginManager(config)
|
||||||
|
pm._init()
|
||||||
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
||||||
|
pm._pluggy_pm.register(obj)
|
||||||
login = config["aprs"]["login"]
|
login = config["aprs"]["login"]
|
||||||
|
|
||||||
packet = {
|
packet = {
|
||||||
@ -198,7 +202,7 @@ def test_plugin(
|
|||||||
"msgNo": 1,
|
"msgNo": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
reply = obj.filter(packet)
|
reply = pm.run(packet)
|
||||||
# Plugin might have threads, so lets stop them so we can exit.
|
# Plugin might have threads, so lets stop them so we can exit.
|
||||||
obj.stop_threads()
|
obj.stop_threads()
|
||||||
LOG.info(f"Result = '{reply}'")
|
LOG.info(f"Result = '{reply}'")
|
||||||
|
@ -17,9 +17,10 @@ from flask_socketio import Namespace, SocketIO
|
|||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import (
|
from aprsd import client
|
||||||
client, kissclient, messaging, packets, plugin, stats, threads, utils,
|
from aprsd import config as aprsd_config
|
||||||
)
|
from aprsd import messaging, packets, plugin, stats, threads, utils
|
||||||
|
from aprsd.clients import aprsis
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -136,7 +137,8 @@ class SendMessageThread(threads.APRSDThread):
|
|||||||
while not connected:
|
while not connected:
|
||||||
try:
|
try:
|
||||||
LOG.info("Creating aprslib client")
|
LOG.info("Creating aprslib client")
|
||||||
aprs_client = client.Aprsdis(
|
|
||||||
|
aprs_client = aprsis.Aprsdis(
|
||||||
user,
|
user,
|
||||||
passwd=password,
|
passwd=password,
|
||||||
host=host,
|
host=host,
|
||||||
@ -312,16 +314,16 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# We might be connected to a KISS socket?
|
# We might be connected to a KISS socket?
|
||||||
if kissclient.KISSClient.kiss_enabled(self.config):
|
if client.KISSClient.kiss_enabled(self.config):
|
||||||
transport = kissclient.KISSClient.transport(self.config)
|
transport = client.KISSClient.transport(self.config)
|
||||||
if transport == kissclient.TRANSPORT_TCPKISS:
|
if transport == client.TRANSPORT_TCPKISS:
|
||||||
aprs_connection = (
|
aprs_connection = (
|
||||||
"TCPKISS://{}:{}".format(
|
"TCPKISS://{}:{}".format(
|
||||||
self.config["kiss"]["tcp"]["host"],
|
self.config["kiss"]["tcp"]["host"],
|
||||||
self.config["kiss"]["tcp"]["port"],
|
self.config["kiss"]["tcp"]["port"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif transport == kissclient.TRANSPORT_SERIALKISS:
|
elif transport == client.TRANSPORT_SERIALKISS:
|
||||||
aprs_connection = (
|
aprs_connection = (
|
||||||
"SerialKISS://{}@{} baud".format(
|
"SerialKISS://{}@{} baud".format(
|
||||||
self.config["kiss"]["serial"]["device"],
|
self.config["kiss"]["serial"]["device"],
|
||||||
@ -338,7 +340,7 @@ class APRSDFlask(flask_classful.FlaskView):
|
|||||||
aprs_connection=aprs_connection,
|
aprs_connection=aprs_connection,
|
||||||
callsign=self.config["aprs"]["login"],
|
callsign=self.config["aprs"]["login"],
|
||||||
version=aprsd.__version__,
|
version=aprsd.__version__,
|
||||||
config_json=json.dumps(self.config),
|
config_json=json.dumps(self.config.data),
|
||||||
watch_count=watch_count,
|
watch_count=watch_count,
|
||||||
watch_age=watch_age,
|
watch_age=watch_age,
|
||||||
plugin_count=plugin_count,
|
plugin_count=plugin_count,
|
||||||
@ -553,10 +555,10 @@ def setup_logging(config, flask_app, loglevel, quiet):
|
|||||||
flask_app.logger.disabled = True
|
flask_app.logger.disabled = True
|
||||||
return
|
return
|
||||||
|
|
||||||
log_level = utils.LOG_LEVELS[loglevel]
|
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||||
LOG.setLevel(log_level)
|
LOG.setLevel(log_level)
|
||||||
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||||
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
log_file = config["aprsd"].get("logfile", None)
|
log_file = config["aprsd"].get("logfile", None)
|
||||||
if log_file:
|
if log_file:
|
||||||
|
@ -19,7 +19,7 @@ import requests
|
|||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import utils
|
from aprsd import config as aprsd_config
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
@ -172,7 +172,7 @@ def parse_delta_str(s):
|
|||||||
"--config",
|
"--config",
|
||||||
"config_file",
|
"config_file",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
default=utils.DEFAULT_CONFIG_FILE,
|
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||||
help="The aprsd config file to use for options.",
|
help="The aprsd config file to use for options.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@ -191,7 +191,7 @@ def parse_delta_str(s):
|
|||||||
def check(loglevel, config_file, health_url, timeout):
|
def check(loglevel, config_file, health_url, timeout):
|
||||||
"""APRSD Plugin test app."""
|
"""APRSD Plugin test app."""
|
||||||
|
|
||||||
config = utils.parse_config(config_file)
|
config = aprsd_config.parse_config(config_file)
|
||||||
|
|
||||||
setup_logging(config, loglevel, False)
|
setup_logging(config, loglevel, False)
|
||||||
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")
|
LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}")
|
||||||
|
@ -36,7 +36,9 @@ import click_completion
|
|||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import client, messaging, stats, threads, trace, utils
|
from aprsd import client
|
||||||
|
from aprsd import config as aprsd_config
|
||||||
|
from aprsd import messaging, stats, threads, trace, utils
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
@ -169,10 +171,10 @@ def signal_handler(sig, frame):
|
|||||||
# to disable logging to stdout, but still log to file
|
# to disable logging to stdout, but still log to file
|
||||||
# use the --quiet option on the cmdln
|
# use the --quiet option on the cmdln
|
||||||
def setup_logging(config, loglevel, quiet):
|
def setup_logging(config, loglevel, quiet):
|
||||||
log_level = utils.LOG_LEVELS[loglevel]
|
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||||
LOG.setLevel(log_level)
|
LOG.setLevel(log_level)
|
||||||
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||||
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
log_file = config["aprsd"].get("logfile", None)
|
log_file = config["aprsd"].get("logfile", None)
|
||||||
if log_file:
|
if log_file:
|
||||||
@ -218,7 +220,7 @@ def setup_logging(config, loglevel, quiet):
|
|||||||
"--config",
|
"--config",
|
||||||
"config_file",
|
"config_file",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
default=utils.DEFAULT_CONFIG_FILE,
|
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||||
help="The aprsd config file to use for options.",
|
help="The aprsd config file to use for options.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@ -258,7 +260,7 @@ def listen(
|
|||||||
"""Send a message to a callsign via APRS_IS."""
|
"""Send a message to a callsign via APRS_IS."""
|
||||||
global got_ack, got_response
|
global got_ack, got_response
|
||||||
|
|
||||||
config = utils.parse_config(config_file)
|
config = aprsd_config.parse_config(config_file)
|
||||||
if not aprs_login:
|
if not aprs_login:
|
||||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||||
return
|
return
|
||||||
|
103
aprsd/main.py
103
aprsd/main.py
@ -37,9 +37,10 @@ import click_completion
|
|||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import (
|
from aprsd import (
|
||||||
client, flask, kissclient, messaging, packets, plugin, stats, threads,
|
flask, messaging, packets, plugin, stats, threads, trace, utils,
|
||||||
trace, utils,
|
|
||||||
)
|
)
|
||||||
|
from aprsd import client
|
||||||
|
from aprsd import config as aprsd_config
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
@ -48,22 +49,8 @@ LOG = logging.getLogger("APRSD")
|
|||||||
|
|
||||||
|
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|
||||||
flask_enabled = False
|
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):
|
def custom_startswith(string, incomplete):
|
||||||
"""A custom completion match that supports case insensitive matching."""
|
"""A custom completion match that supports case insensitive matching."""
|
||||||
@ -172,10 +159,10 @@ def signal_handler(sig, frame):
|
|||||||
# to disable logging to stdout, but still log to file
|
# to disable logging to stdout, but still log to file
|
||||||
# use the --quiet option on the cmdln
|
# use the --quiet option on the cmdln
|
||||||
def setup_logging(config, loglevel, quiet):
|
def setup_logging(config, loglevel, quiet):
|
||||||
log_level = utils.LOG_LEVELS[loglevel]
|
log_level = aprsd_config.LOG_LEVELS[loglevel]
|
||||||
LOG.setLevel(log_level)
|
LOG.setLevel(log_level)
|
||||||
log_format = config["aprsd"].get("logformat", utils.DEFAULT_LOG_FORMAT)
|
log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT)
|
||||||
date_format = config["aprsd"].get("dateformat", utils.DEFAULT_DATE_FORMAT)
|
date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT)
|
||||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
log_file = config["aprsd"].get("logfile", None)
|
log_file = config["aprsd"].get("logfile", None)
|
||||||
if log_file:
|
if log_file:
|
||||||
@ -187,24 +174,17 @@ def setup_logging(config, loglevel, quiet):
|
|||||||
LOG.addHandler(fh)
|
LOG.addHandler(fh)
|
||||||
|
|
||||||
imap_logger = None
|
imap_logger = None
|
||||||
if config["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][
|
if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False):
|
||||||
"imap"
|
|
||||||
].get("debug", False):
|
|
||||||
|
|
||||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||||
imap_logger.setLevel(log_level)
|
imap_logger.setLevel(log_level)
|
||||||
imap_logger.addHandler(fh)
|
imap_logger.addHandler(fh)
|
||||||
|
|
||||||
if (
|
if config.get("aprsd.web.enabled", default=False):
|
||||||
utils.check_config_option(
|
|
||||||
config, ["aprsd", "web", "enabled"],
|
|
||||||
default_fail=False,
|
|
||||||
)
|
|
||||||
):
|
|
||||||
qh = logging.handlers.QueueHandler(threads.logging_queue)
|
qh = logging.handlers.QueueHandler(threads.logging_queue)
|
||||||
q_log_formatter = logging.Formatter(
|
q_log_formatter = logging.Formatter(
|
||||||
fmt=utils.QUEUE_LOG_FORMAT,
|
fmt=aprsd_config.QUEUE_LOG_FORMAT,
|
||||||
datefmt=utils.QUEUE_DATE_FORMAT,
|
datefmt=aprsd_config.QUEUE_DATE_FORMAT,
|
||||||
)
|
)
|
||||||
qh.setFormatter(q_log_formatter)
|
qh.setFormatter(q_log_formatter)
|
||||||
LOG.addHandler(qh)
|
LOG.addHandler(qh)
|
||||||
@ -234,11 +214,11 @@ def setup_logging(config, loglevel, quiet):
|
|||||||
"--config",
|
"--config",
|
||||||
"config_file",
|
"config_file",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
default=utils.DEFAULT_CONFIG_FILE,
|
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||||
help="The aprsd config file to use for options.",
|
help="The aprsd config file to use for options.",
|
||||||
)
|
)
|
||||||
def check_version(loglevel, config_file):
|
def check_version(loglevel, config_file):
|
||||||
config = utils.parse_config(config_file)
|
config = aprsd_config.parse_config(config_file)
|
||||||
|
|
||||||
setup_logging(config, loglevel, False)
|
setup_logging(config, loglevel, False)
|
||||||
level, msg = utils._check_version()
|
level, msg = utils._check_version()
|
||||||
@ -251,7 +231,7 @@ def check_version(loglevel, config_file):
|
|||||||
@main.command()
|
@main.command()
|
||||||
def sample_config():
|
def sample_config():
|
||||||
"""This dumps the config to stdout."""
|
"""This dumps the config to stdout."""
|
||||||
click.echo(utils.dump_default_cfg())
|
click.echo(aprsd_config.dump_default_cfg())
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@ -272,7 +252,7 @@ def sample_config():
|
|||||||
"--config",
|
"--config",
|
||||||
"config_file",
|
"config_file",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
default=utils.DEFAULT_CONFIG_FILE,
|
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||||
help="The aprsd config file to use for options.",
|
help="The aprsd config file to use for options.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@ -312,7 +292,7 @@ def send_message(
|
|||||||
"""Send a message to a callsign via APRS_IS."""
|
"""Send a message to a callsign via APRS_IS."""
|
||||||
global got_ack, got_response
|
global got_ack, got_response
|
||||||
|
|
||||||
config = utils.parse_config(config_file)
|
config = aprsd_config.parse_config(config_file)
|
||||||
if not aprs_login:
|
if not aprs_login:
|
||||||
click.echo("Must set --aprs_login or APRS_LOGIN")
|
click.echo("Must set --aprs_login or APRS_LOGIN")
|
||||||
return
|
return
|
||||||
@ -371,8 +351,8 @@ def send_message(
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cl = client.Client(config)
|
client.ClientFactory.setup(config)
|
||||||
cl.setup_connection()
|
client.factory.create().client
|
||||||
except LoginError:
|
except LoginError:
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
@ -399,7 +379,7 @@ def send_message(
|
|||||||
# This will register a packet consumer with aprslib
|
# This will register a packet consumer with aprslib
|
||||||
# When new packets come in the consumer will process
|
# When new packets come in the consumer will process
|
||||||
# the packet
|
# the packet
|
||||||
aprs_client = client.get_client()
|
aprs_client = client.factory.create().client
|
||||||
aprs_client.consumer(rx_packet, raw=False)
|
aprs_client.consumer(rx_packet, raw=False)
|
||||||
except aprslib.exceptions.ConnectionDrop:
|
except aprslib.exceptions.ConnectionDrop:
|
||||||
LOG.error("Connection dropped, reconnecting")
|
LOG.error("Connection dropped, reconnecting")
|
||||||
@ -407,7 +387,7 @@ def send_message(
|
|||||||
# Force the deletion of the client object connected to aprs
|
# Force the deletion of the client object connected to aprs
|
||||||
# This will cause a reconnect, next time client.get_client()
|
# This will cause a reconnect, next time client.get_client()
|
||||||
# is called
|
# is called
|
||||||
cl.reset()
|
aprs_client.reset()
|
||||||
|
|
||||||
|
|
||||||
# main() ###
|
# main() ###
|
||||||
@ -429,7 +409,7 @@ def send_message(
|
|||||||
"--config",
|
"--config",
|
||||||
"config_file",
|
"config_file",
|
||||||
show_default=True,
|
show_default=True,
|
||||||
default=utils.DEFAULT_CONFIG_FILE,
|
default=aprsd_config.DEFAULT_CONFIG_FILE,
|
||||||
help="The aprsd config file to use for options.",
|
help="The aprsd config file to use for options.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@ -454,7 +434,7 @@ def server(
|
|||||||
if not quiet:
|
if not quiet:
|
||||||
click.echo("Load config")
|
click.echo("Load config")
|
||||||
|
|
||||||
config = utils.parse_config(config_file)
|
config = aprsd_config.parse_config(config_file)
|
||||||
|
|
||||||
setup_logging(config, loglevel, quiet)
|
setup_logging(config, loglevel, quiet)
|
||||||
level, msg = utils._check_version()
|
level, msg = utils._check_version()
|
||||||
@ -476,25 +456,20 @@ def server(
|
|||||||
trace.setup_tracing(["method", "api"])
|
trace.setup_tracing(["method", "api"])
|
||||||
stats.APRSDStats(config)
|
stats.APRSDStats(config)
|
||||||
|
|
||||||
if config["aprs"].get("enabled", True):
|
# Initialize the client factory and create
|
||||||
try:
|
# The correct client object ready for use
|
||||||
cl = client.Client(config)
|
client.ClientFactory.setup(config)
|
||||||
cl.client
|
# Make sure we have 1 client transport enabled
|
||||||
except LoginError:
|
if not client.factory.is_client_enabled():
|
||||||
sys.exit(-1)
|
LOG.error("No Clients are enabled in config.")
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
rx_thread = threads.APRSDRXThread(
|
# Creates the client object
|
||||||
msg_queues=threads.msg_queues,
|
LOG.info("Creating client connection")
|
||||||
config=config,
|
client.factory.create().client
|
||||||
)
|
|
||||||
rx_thread.start()
|
|
||||||
else:
|
|
||||||
LOG.info(
|
|
||||||
"APRS network connection Not Enabled in config. This is"
|
|
||||||
" for setups without internet connectivity.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the initial PM singleton and Register plugins
|
# Create the initial PM singleton and Register plugins
|
||||||
|
LOG.info("Loading Plugin Manager and registering plugins")
|
||||||
plugin_manager = plugin.PluginManager(config)
|
plugin_manager = plugin.PluginManager(config)
|
||||||
plugin_manager.setup_plugins()
|
plugin_manager.setup_plugins()
|
||||||
|
|
||||||
@ -510,20 +485,18 @@ def server(
|
|||||||
packets.PacketList(config=config)
|
packets.PacketList(config=config)
|
||||||
packets.WatchList(config=config)
|
packets.WatchList(config=config)
|
||||||
|
|
||||||
if kissclient.KISSClient.kiss_enabled(config):
|
rx_thread = threads.APRSDRXThread(
|
||||||
kcl = kissclient.KISSClient(config=config)
|
msg_queues=threads.msg_queues,
|
||||||
# This initializes the client object.
|
config=config,
|
||||||
kcl.client
|
)
|
||||||
|
rx_thread.start()
|
||||||
kissrx_thread = threads.KISSRXThread(msg_queues=threads.msg_queues, config=config)
|
|
||||||
kissrx_thread.start()
|
|
||||||
|
|
||||||
messaging.MsgTrack().restart()
|
messaging.MsgTrack().restart()
|
||||||
|
|
||||||
keepalive = threads.KeepAliveThread(config=config)
|
keepalive = threads.KeepAliveThread(config=config)
|
||||||
keepalive.start()
|
keepalive.start()
|
||||||
|
|
||||||
web_enabled = utils.check_config_option(config, ["aprsd", "web", "enabled"], default_fail=False)
|
web_enabled = config.get("aprsd.web.enabled", default=False)
|
||||||
|
|
||||||
if web_enabled:
|
if web_enabled:
|
||||||
flask_enabled = True
|
flask_enabled = True
|
||||||
|
@ -9,7 +9,9 @@ import re
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import client, kissclient, packets, stats, threads, trace, utils
|
from aprsd import client
|
||||||
|
from aprsd import config as aprsd_config
|
||||||
|
from aprsd import packets, stats, threads
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -18,10 +20,6 @@ LOG = logging.getLogger("APRSD")
|
|||||||
# and it's ok, but don't send a usage string back
|
# and it's ok, but don't send a usage string back
|
||||||
NULL_MESSAGE = -1
|
NULL_MESSAGE = -1
|
||||||
|
|
||||||
MESSAGE_TRANSPORT_TCPKISS = "tcpkiss"
|
|
||||||
MESSAGE_TRANSPORT_SERIALKISS = "serialkiss"
|
|
||||||
MESSAGE_TRANSPORT_APRSIS = "aprsis"
|
|
||||||
|
|
||||||
|
|
||||||
class MsgTrack:
|
class MsgTrack:
|
||||||
"""Class to keep track of outstanding text messages.
|
"""Class to keep track of outstanding text messages.
|
||||||
@ -34,13 +32,6 @@ class MsgTrack:
|
|||||||
automatically adds itself to this class. When the ack is
|
automatically adds itself to this class. When the ack is
|
||||||
recieved from the radio, the message object is removed from
|
recieved from the radio, the message object is removed from
|
||||||
this class.
|
this class.
|
||||||
|
|
||||||
# TODO(hemna)
|
|
||||||
When aprsd is asked to quit this class should be serialized and
|
|
||||||
saved to disk/db to keep track of the state of outstanding messages.
|
|
||||||
When aprsd is started, it should try and fetch the saved state,
|
|
||||||
and reloaded to a live state.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
@ -113,11 +104,11 @@ class MsgTrack:
|
|||||||
LOG.debug(f"Save tracker to disk? {len(self)}")
|
LOG.debug(f"Save tracker to disk? {len(self)}")
|
||||||
if len(self) > 0:
|
if len(self) > 0:
|
||||||
LOG.info(f"Saving {len(self)} tracking messages to disk")
|
LOG.info(f"Saving {len(self)} tracking messages to disk")
|
||||||
pickle.dump(self.dump(), open(utils.DEFAULT_SAVE_FILE, "wb+"))
|
pickle.dump(self.dump(), open(aprsd_config.DEFAULT_SAVE_FILE, "wb+"))
|
||||||
else:
|
else:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Nothing to save, flushing old save file '{}'".format(
|
"Nothing to save, flushing old save file '{}'".format(
|
||||||
utils.DEFAULT_SAVE_FILE,
|
aprsd_config.DEFAULT_SAVE_FILE,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.flush()
|
self.flush()
|
||||||
@ -131,8 +122,8 @@ class MsgTrack:
|
|||||||
return dump
|
return dump
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
if os.path.exists(utils.DEFAULT_SAVE_FILE):
|
if os.path.exists(aprsd_config.DEFAULT_SAVE_FILE):
|
||||||
raw = pickle.load(open(utils.DEFAULT_SAVE_FILE, "rb"))
|
raw = pickle.load(open(aprsd_config.DEFAULT_SAVE_FILE, "rb"))
|
||||||
if raw:
|
if raw:
|
||||||
self.track = raw
|
self.track = raw
|
||||||
LOG.debug("Loaded MsgTrack dict from disk.")
|
LOG.debug("Loaded MsgTrack dict from disk.")
|
||||||
@ -171,8 +162,8 @@ class MsgTrack:
|
|||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
"""Nuke the old pickle file that stored the old results from last aprsd run."""
|
"""Nuke the old pickle file that stored the old results from last aprsd run."""
|
||||||
if os.path.exists(utils.DEFAULT_SAVE_FILE):
|
if os.path.exists(aprsd_config.DEFAULT_SAVE_FILE):
|
||||||
pathlib.Path(utils.DEFAULT_SAVE_FILE).unlink()
|
pathlib.Path(aprsd_config.DEFAULT_SAVE_FILE).unlink()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.track = {}
|
self.track = {}
|
||||||
|
|
||||||
@ -239,7 +230,6 @@ class Message(metaclass=abc.ABCMeta):
|
|||||||
fromcall,
|
fromcall,
|
||||||
tocall,
|
tocall,
|
||||||
msg_id=None,
|
msg_id=None,
|
||||||
transport=MESSAGE_TRANSPORT_APRSIS,
|
|
||||||
):
|
):
|
||||||
self.fromcall = fromcall
|
self.fromcall = fromcall
|
||||||
self.tocall = tocall
|
self.tocall = tocall
|
||||||
@ -248,18 +238,11 @@ class Message(metaclass=abc.ABCMeta):
|
|||||||
c.increment()
|
c.increment()
|
||||||
msg_id = c.value
|
msg_id = c.value
|
||||||
self.id = msg_id
|
self.id = msg_id
|
||||||
self.transport = transport
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def send(self):
|
def send(self):
|
||||||
"""Child class must declare."""
|
"""Child class must declare."""
|
||||||
|
|
||||||
def get_transport(self):
|
|
||||||
if self.transport == MESSAGE_TRANSPORT_APRSIS:
|
|
||||||
return client.get_client()
|
|
||||||
elif self.transport == MESSAGE_TRANSPORT_TCPKISS:
|
|
||||||
return kissclient.get_client()
|
|
||||||
|
|
||||||
|
|
||||||
class RawMessage(Message):
|
class RawMessage(Message):
|
||||||
"""Send a raw message.
|
"""Send a raw message.
|
||||||
@ -271,8 +254,8 @@ class RawMessage(Message):
|
|||||||
|
|
||||||
message = None
|
message = None
|
||||||
|
|
||||||
def __init__(self, message, transport=MESSAGE_TRANSPORT_APRSIS):
|
def __init__(self, message):
|
||||||
super().__init__(None, None, msg_id=None, transport=transport)
|
super().__init__(None, None, msg_id=None)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
@ -301,7 +284,7 @@ class RawMessage(Message):
|
|||||||
|
|
||||||
def send_direct(self, aprsis_client=None):
|
def send_direct(self, aprsis_client=None):
|
||||||
"""Send a message without a separate thread."""
|
"""Send a message without a separate thread."""
|
||||||
cl = self.get_transport()
|
cl = client.factory.create().client
|
||||||
log_message(
|
log_message(
|
||||||
"Sending Message Direct",
|
"Sending Message Direct",
|
||||||
str(self).rstrip("\n"),
|
str(self).rstrip("\n"),
|
||||||
@ -310,7 +293,7 @@ class RawMessage(Message):
|
|||||||
fromcall=self.fromcall,
|
fromcall=self.fromcall,
|
||||||
)
|
)
|
||||||
cl.send(self)
|
cl.send(self)
|
||||||
stats.APRSDStats().msgs_sent_inc()
|
stats.APRSDStats().msgs_tx_inc()
|
||||||
|
|
||||||
|
|
||||||
class TextMessage(Message):
|
class TextMessage(Message):
|
||||||
@ -325,9 +308,8 @@ class TextMessage(Message):
|
|||||||
message,
|
message,
|
||||||
msg_id=None,
|
msg_id=None,
|
||||||
allow_delay=True,
|
allow_delay=True,
|
||||||
transport=MESSAGE_TRANSPORT_APRSIS,
|
|
||||||
):
|
):
|
||||||
super().__init__(fromcall, tocall, msg_id, transport=transport)
|
super().__init__(fromcall, tocall, msg_id)
|
||||||
self.message = message
|
self.message = message
|
||||||
# do we try and save this message for later if we don't get
|
# 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.
|
# an ack? Some messages we don't want to do this ever.
|
||||||
@ -384,7 +366,7 @@ class TextMessage(Message):
|
|||||||
if aprsis_client:
|
if aprsis_client:
|
||||||
cl = aprsis_client
|
cl = aprsis_client
|
||||||
else:
|
else:
|
||||||
cl = self.get_transport()
|
cl = client.factory.create().client
|
||||||
log_message(
|
log_message(
|
||||||
"Sending Message Direct",
|
"Sending Message Direct",
|
||||||
str(self).rstrip("\n"),
|
str(self).rstrip("\n"),
|
||||||
@ -422,7 +404,6 @@ class SendMessageThread(threads.APRSDThread):
|
|||||||
LOG.info("Message Send Complete via Ack.")
|
LOG.info("Message Send Complete via Ack.")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
cl = msg.get_transport()
|
|
||||||
send_now = False
|
send_now = False
|
||||||
if msg.last_send_attempt == msg.retry_count:
|
if msg.last_send_attempt == msg.retry_count:
|
||||||
# we reached the send limit, don't send again
|
# we reached the send limit, don't send again
|
||||||
@ -453,6 +434,7 @@ class SendMessageThread(threads.APRSDThread):
|
|||||||
retry_number=msg.last_send_attempt,
|
retry_number=msg.last_send_attempt,
|
||||||
msg_num=msg.id,
|
msg_num=msg.id,
|
||||||
)
|
)
|
||||||
|
cl = client.factory.create().client
|
||||||
cl.send(msg)
|
cl.send(msg)
|
||||||
stats.APRSDStats().msgs_tx_inc()
|
stats.APRSDStats().msgs_tx_inc()
|
||||||
packets.PacketList().add(msg.dict())
|
packets.PacketList().add(msg.dict())
|
||||||
@ -467,8 +449,8 @@ class SendMessageThread(threads.APRSDThread):
|
|||||||
class AckMessage(Message):
|
class AckMessage(Message):
|
||||||
"""Class for building Acks and sending them."""
|
"""Class for building Acks and sending them."""
|
||||||
|
|
||||||
def __init__(self, fromcall, tocall, msg_id, transport=MESSAGE_TRANSPORT_APRSIS):
|
def __init__(self, fromcall, tocall, msg_id):
|
||||||
super().__init__(fromcall, tocall, msg_id=msg_id, transport=transport)
|
super().__init__(fromcall, tocall, msg_id=msg_id)
|
||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
@ -507,7 +489,7 @@ class AckMessage(Message):
|
|||||||
if aprsis_client:
|
if aprsis_client:
|
||||||
cl = aprsis_client
|
cl = aprsis_client
|
||||||
else:
|
else:
|
||||||
cl = self.get_transport()
|
cl = client.factory.create().client
|
||||||
log_message(
|
log_message(
|
||||||
"Sending ack",
|
"Sending ack",
|
||||||
str(self).rstrip("\n"),
|
str(self).rstrip("\n"),
|
||||||
@ -524,10 +506,8 @@ class SendAckThread(threads.APRSDThread):
|
|||||||
self.ack = ack
|
self.ack = ack
|
||||||
super().__init__(f"SendAck-{self.ack.id}")
|
super().__init__(f"SendAck-{self.ack.id}")
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
"""Separate thread to send acks with retries."""
|
"""Separate thread to send acks with retries."""
|
||||||
LOG.debug("SendAckThread loop start")
|
|
||||||
send_now = False
|
send_now = False
|
||||||
if self.ack.last_send_attempt == self.ack.retry_count:
|
if self.ack.last_send_attempt == self.ack.retry_count:
|
||||||
# we reached the send limit, don't send again
|
# we reached the send limit, don't send again
|
||||||
@ -552,7 +532,7 @@ class SendAckThread(threads.APRSDThread):
|
|||||||
send_now = True
|
send_now = True
|
||||||
|
|
||||||
if send_now:
|
if send_now:
|
||||||
cl = self.ack.get_transport()
|
cl = client.factory.create().client
|
||||||
log_message(
|
log_message(
|
||||||
"Sending ack",
|
"Sending ack",
|
||||||
str(self.ack).rstrip("\n"),
|
str(self.ack).rstrip("\n"),
|
||||||
|
@ -17,9 +17,6 @@ from aprsd import client, messaging, packets, threads
|
|||||||
# setup the global logger
|
# setup the global logger
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
hookspec = pluggy.HookspecMarker("aprsd")
|
|
||||||
hookimpl = pluggy.HookimplMarker("aprsd")
|
|
||||||
|
|
||||||
CORE_MESSAGE_PLUGINS = [
|
CORE_MESSAGE_PLUGINS = [
|
||||||
"aprsd.plugins.email.EmailPlugin",
|
"aprsd.plugins.email.EmailPlugin",
|
||||||
"aprsd.plugins.fortune.FortunePlugin",
|
"aprsd.plugins.fortune.FortunePlugin",
|
||||||
@ -36,8 +33,11 @@ CORE_NOTIFY_PLUGINS = [
|
|||||||
"aprsd.plugins.notify.NotifySeenPlugin",
|
"aprsd.plugins.notify.NotifySeenPlugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
hookspec = pluggy.HookspecMarker("aprsd")
|
||||||
|
hookimpl = pluggy.HookimplMarker("aprsd")
|
||||||
|
|
||||||
class APRSDCommandSpec:
|
|
||||||
|
class APRSDPluginSpec:
|
||||||
"""A hook specification namespace."""
|
"""A hook specification namespace."""
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
@ -62,11 +62,8 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.message_counter = 0
|
self.message_counter = 0
|
||||||
self.setup()
|
self.setup()
|
||||||
threads = self.create_threads()
|
self.threads = self.create_threads()
|
||||||
if threads:
|
self.start_threads()
|
||||||
self.threads = threads
|
|
||||||
if self.threads:
|
|
||||||
self.start_threads()
|
|
||||||
|
|
||||||
def start_threads(self):
|
def start_threads(self):
|
||||||
if self.enabled and self.threads:
|
if self.enabled and self.threads:
|
||||||
@ -96,11 +93,6 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||||||
def message_count(self):
|
def message_count(self):
|
||||||
return self.message_counter
|
return self.message_counter
|
||||||
|
|
||||||
@property
|
|
||||||
def version(self):
|
|
||||||
"""Version"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""Do any plugin setup here."""
|
"""Do any plugin setup here."""
|
||||||
@ -122,7 +114,6 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||||||
if isinstance(thread, threads.APRSDThread):
|
if isinstance(thread, threads.APRSDThread):
|
||||||
thread.stop()
|
thread.stop()
|
||||||
|
|
||||||
@hookimpl
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def filter(self, packet):
|
def filter(self, packet):
|
||||||
pass
|
pass
|
||||||
@ -158,20 +149,28 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||||||
)
|
)
|
||||||
# make sure the timeout is set or this doesn't work
|
# make sure the timeout is set or this doesn't work
|
||||||
if watch_list:
|
if watch_list:
|
||||||
aprs_client = client.get_client()
|
aprs_client = client.factory.create().client
|
||||||
filter_str = "b/{}".format("/".join(watch_list))
|
filter_str = "b/{}".format("/".join(watch_list))
|
||||||
aprs_client.set_filter(filter_str)
|
aprs_client.set_filter(filter_str)
|
||||||
else:
|
else:
|
||||||
LOG.warning("Watch list enabled, but no callsigns set.")
|
LOG.warning("Watch list enabled, but no callsigns set.")
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
def filter(self, packet):
|
def filter(self, packet):
|
||||||
|
result = messaging.NULL_MESSAGE
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
wl = packets.WatchList()
|
wl = packets.WatchList()
|
||||||
result = messaging.NULL_MESSAGE
|
|
||||||
if wl.callsign_in_watchlist(packet["from"]):
|
if wl.callsign_in_watchlist(packet["from"]):
|
||||||
# packet is from a callsign in the watch list
|
# packet is from a callsign in the watch list
|
||||||
self.rx_inc()
|
self.rx_inc()
|
||||||
result = self.process()
|
try:
|
||||||
|
result = self.process(packet)
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.error(
|
||||||
|
"Plugin {} failed to process packet {}".format(
|
||||||
|
self.__class__, ex,
|
||||||
|
),
|
||||||
|
)
|
||||||
if result:
|
if result:
|
||||||
self.tx_inc()
|
self.tx_inc()
|
||||||
wl.update_seen(packet)
|
wl.update_seen(packet)
|
||||||
@ -221,7 +220,14 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||||||
if re.search(self.command_regex, message):
|
if re.search(self.command_regex, message):
|
||||||
self.rx_inc()
|
self.rx_inc()
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
result = self.process(packet)
|
try:
|
||||||
|
result = self.process(packet)
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.error(
|
||||||
|
"Plugin {} failed to process packet {}".format(
|
||||||
|
self.__class__, ex,
|
||||||
|
),
|
||||||
|
)
|
||||||
if result:
|
if result:
|
||||||
self.tx_inc()
|
self.tx_inc()
|
||||||
else:
|
else:
|
||||||
@ -255,6 +261,10 @@ class PluginManager:
|
|||||||
if config:
|
if config:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
|
def _init(self):
|
||||||
|
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
||||||
|
self._pluggy_pm.add_hookspecs(APRSDPluginSpec)
|
||||||
|
|
||||||
def load_plugins_from_path(self, module_path):
|
def load_plugins_from_path(self, module_path):
|
||||||
if not os.path.exists(module_path):
|
if not os.path.exists(module_path):
|
||||||
LOG.error(f"plugin path '{module_path}' doesn't exist.")
|
LOG.error(f"plugin path '{module_path}' doesn't exist.")
|
||||||
@ -356,8 +366,7 @@ class PluginManager:
|
|||||||
|
|
||||||
LOG.info("Loading APRSD Plugins")
|
LOG.info("Loading APRSD Plugins")
|
||||||
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
|
enabled_plugins = self.config["aprsd"].get("enabled_plugins", None)
|
||||||
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
self._init()
|
||||||
self._pluggy_pm.add_hookspecs(APRSDCommandSpec)
|
|
||||||
if enabled_plugins:
|
if enabled_plugins:
|
||||||
for p_name in enabled_plugins:
|
for p_name in enabled_plugins:
|
||||||
self._load_plugin(p_name)
|
self._load_plugin(p_name)
|
||||||
|
@ -5,6 +5,7 @@ import imaplib
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import smtplib
|
import smtplib
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import imapclient
|
import imapclient
|
||||||
@ -15,9 +16,44 @@ from aprsd import messaging, plugin, stats, threads, trace
|
|||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
# This gets forced set from main.py prior to being used internally
|
|
||||||
CONFIG = {}
|
class EmailInfo:
|
||||||
check_email_delay = 60
|
"""A singleton thread safe mechanism for the global check_email_delay.
|
||||||
|
|
||||||
|
This has to be done because we have 2 separate threads that access
|
||||||
|
the delay value.
|
||||||
|
1) when EmailPlugin runs from a user message and
|
||||||
|
2) when the background EmailThread runs to check email.
|
||||||
|
|
||||||
|
Access the check email delay with
|
||||||
|
EmailInfo().delay
|
||||||
|
|
||||||
|
Set it with
|
||||||
|
EmailInfo().delay = 100
|
||||||
|
or
|
||||||
|
EmailInfo().delay += 10
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""This magic turns this into a singleton."""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.lock = threading.Lock()
|
||||||
|
cls._instance._delay = 60
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def delay(self):
|
||||||
|
with self.lock:
|
||||||
|
return self._delay
|
||||||
|
|
||||||
|
@delay.setter
|
||||||
|
def delay(self, val):
|
||||||
|
with self.lock:
|
||||||
|
self._delay = val
|
||||||
|
|
||||||
|
|
||||||
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
@ -34,8 +70,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""Ensure that email is enabled and start the thread."""
|
"""Ensure that email is enabled and start the thread."""
|
||||||
global CONFIG
|
|
||||||
CONFIG = self.config
|
|
||||||
|
|
||||||
email_enabled = self.config["aprsd"]["email"].get("enabled", False)
|
email_enabled = self.config["aprsd"]["email"].get("enabled", False)
|
||||||
validation = self.config["aprsd"]["email"].get("validate", False)
|
validation = self.config["aprsd"]["email"].get("validate", False)
|
||||||
@ -81,7 +115,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
r = re.search("^-([0-9])[0-9]*$", message)
|
r = re.search("^-([0-9])[0-9]*$", message)
|
||||||
if r is not None:
|
if r is not None:
|
||||||
LOG.debug("RESEND EMAIL")
|
LOG.debug("RESEND EMAIL")
|
||||||
resend_email(r.group(1), fromcall)
|
resend_email(self.config, r.group(1), fromcall)
|
||||||
reply = messaging.NULL_MESSAGE
|
reply = messaging.NULL_MESSAGE
|
||||||
# -user@address.com body of email
|
# -user@address.com body of email
|
||||||
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
elif re.search(r"^-([A-Za-z0-9_\-\.@]+) (.*)", message):
|
||||||
@ -91,7 +125,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
to_addr = a.group(1)
|
to_addr = a.group(1)
|
||||||
content = a.group(2)
|
content = a.group(2)
|
||||||
|
|
||||||
email_address = get_email_from_shortcut(to_addr)
|
email_address = get_email_from_shortcut(self.config, to_addr)
|
||||||
if not email_address:
|
if not email_address:
|
||||||
reply = "Bad email address"
|
reply = "Bad email address"
|
||||||
return reply
|
return reply
|
||||||
@ -114,7 +148,7 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
too_soon = 1
|
too_soon = 1
|
||||||
if not too_soon or ack == 0:
|
if not too_soon or ack == 0:
|
||||||
LOG.info(f"Send email '{content}'")
|
LOG.info(f"Send email '{content}'")
|
||||||
send_result = email.send_email(to_addr, content)
|
send_result = send_email(self.config, to_addr, content)
|
||||||
reply = messaging.NULL_MESSAGE
|
reply = messaging.NULL_MESSAGE
|
||||||
if send_result != 0:
|
if send_result != 0:
|
||||||
reply = f"-{to_addr} failed"
|
reply = f"-{to_addr} failed"
|
||||||
@ -143,10 +177,9 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
return reply
|
return reply
|
||||||
|
|
||||||
|
|
||||||
def _imap_connect():
|
def _imap_connect(config):
|
||||||
global CONFIG
|
imap_port = config["aprsd"]["email"]["imap"].get("port", 143)
|
||||||
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
|
use_ssl = config["aprsd"]["email"]["imap"].get("use_ssl", False)
|
||||||
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
|
|
||||||
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
|
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
|
||||||
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
|
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
|
||||||
# LOG.debug("Connect to IMAP host {} with user '{}'".
|
# LOG.debug("Connect to IMAP host {} with user '{}'".
|
||||||
@ -154,7 +187,7 @@ def _imap_connect():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
server = imapclient.IMAPClient(
|
server = imapclient.IMAPClient(
|
||||||
CONFIG["aprsd"]["email"]["imap"]["host"],
|
config["aprsd"]["email"]["imap"]["host"],
|
||||||
port=imap_port,
|
port=imap_port,
|
||||||
use_uid=True,
|
use_uid=True,
|
||||||
ssl=use_ssl,
|
ssl=use_ssl,
|
||||||
@ -166,8 +199,8 @@ def _imap_connect():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
server.login(
|
server.login(
|
||||||
CONFIG["aprsd"]["email"]["imap"]["login"],
|
config["aprsd"]["email"]["imap"]["login"],
|
||||||
CONFIG["aprsd"]["email"]["imap"]["password"],
|
config["aprsd"]["email"]["imap"]["password"],
|
||||||
)
|
)
|
||||||
except (imaplib.IMAP4.error, Exception) as e:
|
except (imaplib.IMAP4.error, Exception) as e:
|
||||||
msg = getattr(e, "message", repr(e))
|
msg = getattr(e, "message", repr(e))
|
||||||
@ -183,15 +216,15 @@ def _imap_connect():
|
|||||||
return server
|
return server
|
||||||
|
|
||||||
|
|
||||||
def _smtp_connect():
|
def _smtp_connect(config):
|
||||||
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
|
host = config["aprsd"]["email"]["smtp"]["host"]
|
||||||
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
|
smtp_port = config["aprsd"]["email"]["smtp"]["port"]
|
||||||
use_ssl = CONFIG["aprsd"]["email"]["smtp"].get("use_ssl", False)
|
use_ssl = config["aprsd"]["email"]["smtp"].get("use_ssl", False)
|
||||||
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
|
msg = "{}{}:{}".format("SSL " if use_ssl else "", host, smtp_port)
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"Connect to SMTP host {} with user '{}'".format(
|
"Connect to SMTP host {} with user '{}'".format(
|
||||||
msg,
|
msg,
|
||||||
CONFIG["aprsd"]["email"]["imap"]["login"],
|
config["aprsd"]["email"]["imap"]["login"],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -214,15 +247,15 @@ def _smtp_connect():
|
|||||||
|
|
||||||
LOG.debug(f"Connected to smtp host {msg}")
|
LOG.debug(f"Connected to smtp host {msg}")
|
||||||
|
|
||||||
debug = CONFIG["aprsd"]["email"]["smtp"].get("debug", False)
|
debug = config["aprsd"]["email"]["smtp"].get("debug", False)
|
||||||
if debug:
|
if debug:
|
||||||
server.set_debuglevel(5)
|
server.set_debuglevel(5)
|
||||||
server.sendmail = trace.trace(server.sendmail)
|
server.sendmail = trace.trace(server.sendmail)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server.login(
|
server.login(
|
||||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
config["aprsd"]["email"]["smtp"]["login"],
|
||||||
CONFIG["aprsd"]["email"]["smtp"]["password"],
|
config["aprsd"]["email"]["smtp"]["password"],
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.error("Couldn't connect to SMTP Server")
|
LOG.error("Couldn't connect to SMTP Server")
|
||||||
@ -273,9 +306,9 @@ def validate_shortcuts(config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_email_from_shortcut(addr):
|
def get_email_from_shortcut(config, addr):
|
||||||
if CONFIG["aprsd"]["email"].get("shortcuts", False):
|
if config["aprsd"]["email"].get("shortcuts", False):
|
||||||
return CONFIG["aprsd"]["email"]["shortcuts"].get(addr, addr)
|
return config["aprsd"]["email"]["shortcuts"].get(addr, addr)
|
||||||
else:
|
else:
|
||||||
return addr
|
return addr
|
||||||
|
|
||||||
@ -286,9 +319,9 @@ def validate_email_config(config, disable_validation=False):
|
|||||||
This helps with failing early during startup.
|
This helps with failing early during startup.
|
||||||
"""
|
"""
|
||||||
LOG.info("Checking IMAP configuration")
|
LOG.info("Checking IMAP configuration")
|
||||||
imap_server = _imap_connect()
|
imap_server = _imap_connect(config)
|
||||||
LOG.info("Checking SMTP configuration")
|
LOG.info("Checking SMTP configuration")
|
||||||
smtp_server = _smtp_connect()
|
smtp_server = _smtp_connect(config)
|
||||||
|
|
||||||
# Now validate and flag any shortcuts as invalid
|
# Now validate and flag any shortcuts as invalid
|
||||||
if not disable_validation:
|
if not disable_validation:
|
||||||
@ -398,34 +431,32 @@ def parse_email(msgid, data, server):
|
|||||||
|
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def send_email(to_addr, content):
|
def send_email(config, to_addr, content):
|
||||||
global check_email_delay
|
shortcuts = config["aprsd"]["email"]["shortcuts"]
|
||||||
|
email_address = get_email_from_shortcut(config, to_addr)
|
||||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
|
||||||
email_address = get_email_from_shortcut(to_addr)
|
|
||||||
LOG.info("Sending Email_________________")
|
LOG.info("Sending Email_________________")
|
||||||
|
|
||||||
if to_addr in shortcuts:
|
if to_addr in shortcuts:
|
||||||
LOG.info("To : " + to_addr)
|
LOG.info("To : " + to_addr)
|
||||||
to_addr = email_address
|
to_addr = email_address
|
||||||
LOG.info(" (" + to_addr + ")")
|
LOG.info(" (" + to_addr + ")")
|
||||||
subject = CONFIG["ham"]["callsign"]
|
subject = config["ham"]["callsign"]
|
||||||
# content = content + "\n\n(NOTE: reply with one line)"
|
# content = content + "\n\n(NOTE: reply with one line)"
|
||||||
LOG.info("Subject : " + subject)
|
LOG.info("Subject : " + subject)
|
||||||
LOG.info("Body : " + content)
|
LOG.info("Body : " + content)
|
||||||
|
|
||||||
# check email more often since there's activity right now
|
# check email more often since there's activity right now
|
||||||
check_email_delay = 60
|
EmailInfo().delay = 60
|
||||||
|
|
||||||
msg = MIMEText(content)
|
msg = MIMEText(content)
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
msg["From"] = CONFIG["aprsd"]["email"]["smtp"]["login"]
|
msg["From"] = config["aprsd"]["email"]["smtp"]["login"]
|
||||||
msg["To"] = to_addr
|
msg["To"] = to_addr
|
||||||
server = _smtp_connect()
|
server = _smtp_connect()
|
||||||
if server:
|
if server:
|
||||||
try:
|
try:
|
||||||
server.sendmail(
|
server.sendmail(
|
||||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
config["aprsd"]["email"]["smtp"]["login"],
|
||||||
[to_addr],
|
[to_addr],
|
||||||
msg.as_string(),
|
msg.as_string(),
|
||||||
)
|
)
|
||||||
@ -440,20 +471,19 @@ def send_email(to_addr, content):
|
|||||||
|
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def resend_email(count, fromcall):
|
def resend_email(config, count, fromcall):
|
||||||
global check_email_delay
|
|
||||||
date = datetime.datetime.now()
|
date = datetime.datetime.now()
|
||||||
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
month = date.strftime("%B")[:3] # Nov, Mar, Apr
|
||||||
day = date.day
|
day = date.day
|
||||||
year = date.year
|
year = date.year
|
||||||
today = f"{day}-{month}-{year}"
|
today = f"{day}-{month}-{year}"
|
||||||
|
|
||||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
shortcuts = config["aprsd"]["email"]["shortcuts"]
|
||||||
# swap key/value
|
# swap key/value
|
||||||
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = _imap_connect()
|
server = _imap_connect(config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
|
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
|
||||||
return
|
return
|
||||||
@ -493,7 +523,7 @@ def resend_email(count, fromcall):
|
|||||||
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
|
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
|
||||||
# messaging.send_message(fromcall, reply)
|
# messaging.send_message(fromcall, reply)
|
||||||
msg = messaging.TextMessage(
|
msg = messaging.TextMessage(
|
||||||
CONFIG["aprs"]["login"],
|
config["aprs"]["login"],
|
||||||
fromcall,
|
fromcall,
|
||||||
reply,
|
reply,
|
||||||
)
|
)
|
||||||
@ -515,11 +545,11 @@ def resend_email(count, fromcall):
|
|||||||
str(s).zfill(2),
|
str(s).zfill(2),
|
||||||
)
|
)
|
||||||
# messaging.send_message(fromcall, reply)
|
# messaging.send_message(fromcall, reply)
|
||||||
msg = messaging.TextMessage(CONFIG["aprs"]["login"], fromcall, reply)
|
msg = messaging.TextMessage(config["aprs"]["login"], fromcall, reply)
|
||||||
msg.send()
|
msg.send()
|
||||||
|
|
||||||
# check email more often since we're resending one now
|
# check email more often since we're resending one now
|
||||||
check_email_delay = 60
|
EmailInfo().delay = 60
|
||||||
|
|
||||||
server.logout()
|
server.logout()
|
||||||
# end resend_email()
|
# end resend_email()
|
||||||
@ -533,27 +563,24 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
self.past = datetime.datetime.now()
|
self.past = datetime.datetime.now()
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
global check_email_delay
|
|
||||||
|
|
||||||
check_email_delay = 60
|
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
stats.APRSDStats().email_thread_update()
|
stats.APRSDStats().email_thread_update()
|
||||||
# always sleep for 5 seconds and see if we need to check email
|
# always sleep for 5 seconds and see if we need to check email
|
||||||
# This allows CTRL-C to stop the execution of this loop sooner
|
# This allows CTRL-C to stop the execution of this loop sooner
|
||||||
# than check_email_delay time
|
# than check_email_delay time
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
if now - self.past > datetime.timedelta(seconds=check_email_delay):
|
if now - self.past > datetime.timedelta(seconds=EmailInfo().delay):
|
||||||
# It's time to check email
|
# It's time to check email
|
||||||
|
|
||||||
# slowly increase delay every iteration, max out at 300 seconds
|
# slowly increase delay every iteration, max out at 300 seconds
|
||||||
# any send/receive/resend activity will reset this to 60 seconds
|
# any send/receive/resend activity will reset this to 60 seconds
|
||||||
if check_email_delay < 300:
|
if EmailInfo().delay < 300:
|
||||||
check_email_delay += 1
|
EmailInfo().delay += 10
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"check_email_delay is " + str(check_email_delay) + " seconds",
|
f"check_email_delay is {EmailInfo().delay} seconds ",
|
||||||
)
|
)
|
||||||
|
|
||||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
shortcuts = self.config["aprsd"]["email"]["shortcuts"]
|
||||||
# swap key/value
|
# swap key/value
|
||||||
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||||
|
|
||||||
@ -564,7 +591,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
today = f"{day}-{month}-{year}"
|
today = f"{day}-{month}-{year}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = _imap_connect()
|
server = _imap_connect(self.config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception("IMAP failed to connect.", e)
|
LOG.exception("IMAP failed to connect.", e)
|
||||||
return True
|
return True
|
||||||
@ -658,7 +685,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
LOG.exception("Couldn't remove seen flag from email", e)
|
LOG.exception("Couldn't remove seen flag from email", e)
|
||||||
|
|
||||||
# check email more often since we just received an email
|
# check email more often since we just received an email
|
||||||
check_email_delay = 60
|
EmailInfo().delay = 60
|
||||||
|
|
||||||
# reset clock
|
# reset clock
|
||||||
LOG.debug("Done looping over Server.fetch, logging out.")
|
LOG.debug("Done looping over Server.fetch, logging out.")
|
||||||
|
@ -2,7 +2,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprsd import plugin, plugin_utils, trace, utils
|
from aprsd import plugin, plugin_utils, trace
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -24,7 +24,7 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
|
|
||||||
# get last location of a callsign, get descriptive name from weather service
|
# get last location of a callsign, get descriptive name from weather service
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
self.config.check_option(["services", "aprs.fi", "apiKey"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||||
return "No aprs.fi apikey found"
|
return "No aprs.fi apikey found"
|
||||||
|
@ -17,12 +17,8 @@ class NotifySeenPlugin(plugin.APRSDWatchListPluginBase):
|
|||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
"""The aprsd config object is stored."""
|
|
||||||
super().__init__(config)
|
|
||||||
|
|
||||||
def process(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("BaseNotifyPlugin")
|
LOG.info("NotifySeenPlugin")
|
||||||
|
|
||||||
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"]
|
notify_callsign = self.config["aprsd"]["watch_list"]["alert_callsign"]
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
|
@ -5,7 +5,7 @@ import time
|
|||||||
from opencage.geocoder import OpenCageGeocode
|
from opencage.geocoder import OpenCageGeocode
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from aprsd import fuzzyclock, plugin, plugin_utils, trace, utils
|
from aprsd import fuzzyclock, plugin, plugin_utils, trace
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -64,7 +64,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
|||||||
|
|
||||||
# get last location of a callsign, get descriptive name from weather service
|
# get last location of a callsign, get descriptive name from weather service
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||||
return "No aprs.fi apikey found"
|
return "No aprs.fi apikey found"
|
||||||
@ -95,7 +95,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
|||||||
lon = aprs_data["entries"][0]["lng"]
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, "opencagedata", "apiKey")
|
self.config.exists("opencagedata.apiKey")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config opencage:apiKey {ex}")
|
LOG.error(f"Failed to find config opencage:apiKey {ex}")
|
||||||
return "No opencage apiKey found"
|
return "No opencage apiKey found"
|
||||||
@ -130,7 +130,7 @@ class TimeOWMPlugin(TimePlugin):
|
|||||||
|
|
||||||
# get last location of a callsign, get descriptive name from weather service
|
# get last location of a callsign, get descriptive name from weather service
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||||
return "No aprs.fi apikey found"
|
return "No aprs.fi apikey found"
|
||||||
@ -160,8 +160,7 @@ class TimeOWMPlugin(TimePlugin):
|
|||||||
lon = aprs_data["entries"][0]["lng"]
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(
|
self.config.exists(
|
||||||
self.config,
|
|
||||||
["services", "openweathermap", "apiKey"],
|
["services", "openweathermap", "apiKey"],
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
@ -4,7 +4,7 @@ import re
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from aprsd import plugin, plugin_utils, trace, utils
|
from aprsd import plugin, plugin_utils, trace
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -34,7 +34,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
# message = packet.get("message_text", None)
|
# message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||||
return "No aprs.fi apikey found"
|
return "No aprs.fi apikey found"
|
||||||
@ -115,10 +115,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
fromcall = fromcall
|
fromcall = fromcall
|
||||||
|
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(
|
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||||
self.config,
|
|
||||||
["services", "aprs.fi", "apiKey"],
|
|
||||||
)
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||||
return "No aprs.fi apikey found"
|
return "No aprs.fi apikey found"
|
||||||
@ -199,7 +196,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
searchcall = fromcall
|
searchcall = fromcall
|
||||||
|
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||||
return "No aprs.fi apikey found"
|
return "No aprs.fi apikey found"
|
||||||
@ -220,16 +217,13 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
lon = aprs_data["entries"][0]["lng"]
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(
|
self.config.exists(["services", "openweathermap", "apiKey"])
|
||||||
self.config,
|
|
||||||
["services", "openweathermap", "apiKey"],
|
|
||||||
)
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config openweathermap:apiKey {ex}")
|
LOG.error(f"Failed to find config openweathermap:apiKey {ex}")
|
||||||
return "No openweathermap apiKey found"
|
return "No openweathermap apiKey found"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["aprsd", "units"])
|
self.config.exists(["aprsd", "units"])
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.debug("Couldn't find untis in aprsd:services:units")
|
LOG.debug("Couldn't find untis in aprsd:services:units")
|
||||||
units = "metric"
|
units = "metric"
|
||||||
@ -323,7 +317,7 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
searchcall = fromcall
|
searchcall = fromcall
|
||||||
|
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "aprs.fi", "apiKey"])
|
self.config.exists(["services", "aprs.fi", "apiKey"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
LOG.error(f"Failed to find config aprs.fi:apikey {ex}")
|
||||||
return "No aprs.fi apikey found"
|
return "No aprs.fi apikey found"
|
||||||
@ -344,13 +338,13 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
lon = aprs_data["entries"][0]["lng"]
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "avwx", "apiKey"])
|
self.config.exists(["services", "avwx", "apiKey"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to find config avwx:apiKey {ex}")
|
LOG.error(f"Failed to find config avwx:apiKey {ex}")
|
||||||
return "No avwx apiKey found"
|
return "No avwx apiKey found"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
utils.check_config_option(self.config, ["services", "avwx", "base_url"])
|
self.config.exists(self.config, ["services", "avwx", "base_url"])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.debug(f"Didn't find avwx:base_url {ex}")
|
LOG.debug(f"Didn't find avwx:base_url {ex}")
|
||||||
base_url = "https://avwx.rest"
|
base_url = "https://avwx.rest"
|
||||||
|
107
aprsd/threads.py
107
aprsd/threads.py
@ -8,7 +8,7 @@ import tracemalloc
|
|||||||
|
|
||||||
import aprslib
|
import aprslib
|
||||||
|
|
||||||
from aprsd import client, kissclient, messaging, packets, plugin, stats, utils
|
from aprsd import client, messaging, packets, plugin, stats, utils
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -137,9 +137,9 @@ class KeepAliveThread(APRSDThread):
|
|||||||
if delta > self.max_delta:
|
if delta > self.max_delta:
|
||||||
# We haven't gotten a keepalive from aprs-is in a while
|
# We haven't gotten a keepalive from aprs-is in a while
|
||||||
# reset the connection.a
|
# reset the connection.a
|
||||||
if not kissclient.KISSClient.kiss_enabled(self.config):
|
if not client.KISSClient.is_enabled(self.config):
|
||||||
LOG.warning("Resetting connection to APRS-IS.")
|
LOG.warning("Resetting connection to APRS-IS.")
|
||||||
client.Client().reset()
|
client.factory.create().reset()
|
||||||
|
|
||||||
# Check version every hour
|
# Check version every hour
|
||||||
delta = now - self.checker_time
|
delta = now - self.checker_time
|
||||||
@ -158,13 +158,13 @@ class APRSDRXThread(APRSDThread):
|
|||||||
super().__init__("RX_MSG")
|
super().__init__("RX_MSG")
|
||||||
self.msg_queues = msg_queues
|
self.msg_queues = msg_queues
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self._client = client.factory.create()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.thread_stop = True
|
self.thread_stop = True
|
||||||
client.get_client().stop()
|
client.factory.create().client.stop()
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
aprs_client = client.get_client()
|
|
||||||
|
|
||||||
# setup the consumer of messages and block until a messages
|
# setup the consumer of messages and block until a messages
|
||||||
try:
|
try:
|
||||||
@ -177,7 +177,9 @@ class APRSDRXThread(APRSDThread):
|
|||||||
# and the aprslib developer didn't want to allow a PR to add
|
# and the aprslib developer didn't want to allow a PR to add
|
||||||
# kwargs. :(
|
# kwargs. :(
|
||||||
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
||||||
aprs_client.consumer(self.process_packet, raw=False, blocking=False)
|
self._client.client.consumer(
|
||||||
|
self.process_packet, raw=False, blocking=False,
|
||||||
|
)
|
||||||
|
|
||||||
except aprslib.exceptions.ConnectionDrop:
|
except aprslib.exceptions.ConnectionDrop:
|
||||||
LOG.error("Connection dropped, reconnecting")
|
LOG.error("Connection dropped, reconnecting")
|
||||||
@ -185,21 +187,21 @@ class APRSDRXThread(APRSDThread):
|
|||||||
# Force the deletion of the client object connected to aprs
|
# Force the deletion of the client object connected to aprs
|
||||||
# This will cause a reconnect, next time client.get_client()
|
# This will cause a reconnect, next time client.get_client()
|
||||||
# is called
|
# is called
|
||||||
client.Client().reset()
|
self._client.reset()
|
||||||
# Continue to loop
|
# Continue to loop
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def process_packet(self, packet):
|
def process_packet(self, *args, **kwargs):
|
||||||
|
packet = self._client.decode_packet(*args, **kwargs)
|
||||||
thread = APRSDProcessPacketThread(packet=packet, config=self.config)
|
thread = APRSDProcessPacketThread(packet=packet, config=self.config)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
class APRSDProcessPacketThread(APRSDThread):
|
class APRSDProcessPacketThread(APRSDThread):
|
||||||
|
|
||||||
def __init__(self, packet, config, transport="aprsis"):
|
def __init__(self, packet, config):
|
||||||
self.packet = packet
|
self.packet = packet
|
||||||
self.config = config
|
self.config = config
|
||||||
self.transport = transport
|
|
||||||
name = self.packet["raw"][:10]
|
name = self.packet["raw"][:10]
|
||||||
super().__init__(f"RX_PACKET-{name}")
|
super().__init__(f"RX_PACKET-{name}")
|
||||||
|
|
||||||
@ -254,7 +256,6 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
self.config["aprs"]["login"],
|
self.config["aprs"]["login"],
|
||||||
fromcall,
|
fromcall,
|
||||||
msg_id=msg_id,
|
msg_id=msg_id,
|
||||||
transport=self.transport,
|
|
||||||
)
|
)
|
||||||
ack.send()
|
ack.send()
|
||||||
|
|
||||||
@ -275,7 +276,6 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
self.config["aprs"]["login"],
|
self.config["aprs"]["login"],
|
||||||
fromcall,
|
fromcall,
|
||||||
subreply,
|
subreply,
|
||||||
transport=self.transport,
|
|
||||||
)
|
)
|
||||||
msg.send()
|
msg.send()
|
||||||
elif isinstance(reply, messaging.Message):
|
elif isinstance(reply, messaging.Message):
|
||||||
@ -296,7 +296,6 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
self.config["aprs"]["login"],
|
self.config["aprs"]["login"],
|
||||||
fromcall,
|
fromcall,
|
||||||
reply,
|
reply,
|
||||||
transport=self.transport,
|
|
||||||
)
|
)
|
||||||
msg.send()
|
msg.send()
|
||||||
|
|
||||||
@ -309,7 +308,6 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
self.config["aprs"]["login"],
|
self.config["aprs"]["login"],
|
||||||
fromcall,
|
fromcall,
|
||||||
reply,
|
reply,
|
||||||
transport=self.transport,
|
|
||||||
)
|
)
|
||||||
msg.send()
|
msg.send()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@ -321,88 +319,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
self.config["aprs"]["login"],
|
self.config["aprs"]["login"],
|
||||||
fromcall,
|
fromcall,
|
||||||
reply,
|
reply,
|
||||||
transport=self.transport,
|
|
||||||
)
|
)
|
||||||
msg.send()
|
msg.send()
|
||||||
|
|
||||||
LOG.debug("Packet processing complete")
|
LOG.debug("Packet processing complete")
|
||||||
|
|
||||||
|
|
||||||
class APRSDTXThread(APRSDThread):
|
|
||||||
def __init__(self, msg_queues, config):
|
|
||||||
super().__init__("TX_MSG")
|
|
||||||
self.msg_queues = msg_queues
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def loop(self):
|
|
||||||
try:
|
|
||||||
msg = self.msg_queues["tx"].get(timeout=1)
|
|
||||||
msg.send()
|
|
||||||
except queue.Empty:
|
|
||||||
pass
|
|
||||||
# Continue to loop
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class KISSRXThread(APRSDThread):
|
|
||||||
"""Thread that connects to direwolf's TCPKISS interface.
|
|
||||||
|
|
||||||
All Packets are processed and sent back out the direwolf
|
|
||||||
interface instead of the aprs-is server.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, msg_queues, config):
|
|
||||||
super().__init__("KISSRX_MSG")
|
|
||||||
self.msg_queues = msg_queues
|
|
||||||
self.config = config
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self.thread_stop = True
|
|
||||||
kissclient.get_client().stop()
|
|
||||||
|
|
||||||
def loop(self):
|
|
||||||
kiss_client = kissclient.get_client()
|
|
||||||
|
|
||||||
# setup the consumer of messages and block until a messages
|
|
||||||
try:
|
|
||||||
# This will register a packet consumer with aprslib
|
|
||||||
# When new packets come in the consumer will process
|
|
||||||
# the packet
|
|
||||||
|
|
||||||
# Do a partial here because the consumer signature doesn't allow
|
|
||||||
# For kwargs to be passed in to the consumer func we declare
|
|
||||||
# and the aprslib developer didn't want to allow a PR to add
|
|
||||||
# kwargs. :(
|
|
||||||
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
|
||||||
kiss_client.consumer(self.process_packet, callsign=self.config["kiss"]["callsign"])
|
|
||||||
kiss_client.loop.run_forever()
|
|
||||||
|
|
||||||
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
|
|
||||||
client.Client().reset()
|
|
||||||
# Continue to loop
|
|
||||||
|
|
||||||
def process_packet(self, interface, frame):
|
|
||||||
"""Process a packet recieved from aprs-is server."""
|
|
||||||
|
|
||||||
LOG.debug(f"Got an APRS Frame '{frame}'")
|
|
||||||
# try and nuke the * from the fromcall sign.
|
|
||||||
frame.header._source._ch = False
|
|
||||||
payload = str(frame.payload.decode())
|
|
||||||
msg = f"{str(frame.header)}:{payload}"
|
|
||||||
# msg = frame.tnc2
|
|
||||||
LOG.debug(f"Decoding {msg}")
|
|
||||||
|
|
||||||
packet = aprslib.parse(msg)
|
|
||||||
LOG.debug(packet)
|
|
||||||
thread = APRSDProcessPacketThread(
|
|
||||||
packet=packet, config=self.config,
|
|
||||||
transport=messaging.MESSAGE_TRANSPORT_TCPKISS,
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
return
|
|
||||||
|
348
aprsd/utils.py
348
aprsd/utils.py
@ -3,128 +3,13 @@
|
|||||||
import collections
|
import collections
|
||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import click
|
|
||||||
import update_checker
|
import update_checker
|
||||||
import yaml
|
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import plugin
|
|
||||||
|
|
||||||
|
|
||||||
LOG_LEVELS = {
|
|
||||||
"CRITICAL": logging.CRITICAL,
|
|
||||||
"ERROR": logging.ERROR,
|
|
||||||
"WARNING": logging.WARNING,
|
|
||||||
"INFO": logging.INFO,
|
|
||||||
"DEBUG": logging.DEBUG,
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
|
|
||||||
DEFAULT_LOG_FORMAT = (
|
|
||||||
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
|
||||||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
|
||||||
)
|
|
||||||
|
|
||||||
QUEUE_DATE_FORMAT = "[%m/%d/%Y] [%I:%M:%S %p]"
|
|
||||||
QUEUE_LOG_FORMAT = (
|
|
||||||
"%(asctime)s [%(threadName)-20.20s] [%(levelname)-5.5s]"
|
|
||||||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
|
||||||
)
|
|
||||||
|
|
||||||
# an example of what should be in the ~/.aprsd/config.yml
|
|
||||||
DEFAULT_CONFIG_DICT = {
|
|
||||||
"ham": {"callsign": "NOCALL"},
|
|
||||||
"aprs": {
|
|
||||||
"enabled": True,
|
|
||||||
"login": "CALLSIGN",
|
|
||||||
"password": "00000",
|
|
||||||
"host": "rotate.aprs2.net",
|
|
||||||
"port": 14580,
|
|
||||||
},
|
|
||||||
"kiss": {
|
|
||||||
"tcp": {
|
|
||||||
"enabled": False,
|
|
||||||
"host": "direwolf.ip.address",
|
|
||||||
"port": "8001",
|
|
||||||
},
|
|
||||||
"serial": {
|
|
||||||
"enabled": False,
|
|
||||||
"device": "/dev/ttyS0",
|
|
||||||
"baudrate": 9600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"aprsd": {
|
|
||||||
"logfile": "/tmp/aprsd.log",
|
|
||||||
"logformat": DEFAULT_LOG_FORMAT,
|
|
||||||
"dateformat": DEFAULT_DATE_FORMAT,
|
|
||||||
"trace": False,
|
|
||||||
"enabled_plugins": plugin.CORE_MESSAGE_PLUGINS,
|
|
||||||
"units": "imperial",
|
|
||||||
"watch_list": {
|
|
||||||
"enabled": False,
|
|
||||||
# Who gets the alert?
|
|
||||||
"alert_callsign": "NOCALL",
|
|
||||||
# 43200 is 12 hours
|
|
||||||
"alert_time_seconds": 43200,
|
|
||||||
# How many packets to save in a ring Buffer
|
|
||||||
# for a particular callsign
|
|
||||||
"packet_keep_count": 10,
|
|
||||||
"callsigns": [],
|
|
||||||
"enabled_plugins": plugin.CORE_NOTIFY_PLUGINS,
|
|
||||||
},
|
|
||||||
"web": {
|
|
||||||
"enabled": True,
|
|
||||||
"logging_enabled": True,
|
|
||||||
"host": "0.0.0.0",
|
|
||||||
"port": 8001,
|
|
||||||
"users": {
|
|
||||||
"admin": "password-here",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"enabled": True,
|
|
||||||
"shortcuts": {
|
|
||||||
"aa": "5551239999@vtext.com",
|
|
||||||
"cl": "craiglamparter@somedomain.org",
|
|
||||||
"wb": "555309@vtext.com",
|
|
||||||
},
|
|
||||||
"smtp": {
|
|
||||||
"login": "SMTP_USERNAME",
|
|
||||||
"password": "SMTP_PASSWORD",
|
|
||||||
"host": "smtp.gmail.com",
|
|
||||||
"port": 465,
|
|
||||||
"use_ssl": False,
|
|
||||||
"debug": False,
|
|
||||||
},
|
|
||||||
"imap": {
|
|
||||||
"login": "IMAP_USERNAME",
|
|
||||||
"password": "IMAP_PASSWORD",
|
|
||||||
"host": "imap.gmail.com",
|
|
||||||
"port": 993,
|
|
||||||
"use_ssl": True,
|
|
||||||
"debug": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"services": {
|
|
||||||
"aprs.fi": {"apiKey": "APIKEYVALUE"},
|
|
||||||
"openweathermap": {"apiKey": "APIKEYVALUE"},
|
|
||||||
"opencagedata": {"apiKey": "APIKEYVALUE"},
|
|
||||||
"avwx": {"base_url": "http://host:port", "apiKey": "APIKEYVALUE"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
home = str(Path.home())
|
|
||||||
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
|
|
||||||
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
|
|
||||||
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.yml"
|
|
||||||
|
|
||||||
|
|
||||||
def synchronized(wrapped):
|
def synchronized(wrapped):
|
||||||
@ -175,239 +60,6 @@ def end_substr(original, substr):
|
|||||||
return idx
|
return idx
|
||||||
|
|
||||||
|
|
||||||
def dump_default_cfg():
|
|
||||||
return add_config_comments(
|
|
||||||
yaml.dump(
|
|
||||||
DEFAULT_CONFIG_DICT,
|
|
||||||
indent=4,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def add_config_comments(raw_yaml):
|
|
||||||
end_idx = end_substr(raw_yaml, "aprs:")
|
|
||||||
if end_idx != -1:
|
|
||||||
# lets insert a comment
|
|
||||||
raw_yaml = insert_str(
|
|
||||||
raw_yaml,
|
|
||||||
"\n # Set enabled to False if there is no internet connectivity."
|
|
||||||
"\n # This is useful for a direwolf KISS aprs connection only. "
|
|
||||||
"\n"
|
|
||||||
"\n # Get the passcode for your callsign here: "
|
|
||||||
"\n # https://apps.magicbug.co.uk/passcode",
|
|
||||||
end_idx,
|
|
||||||
)
|
|
||||||
|
|
||||||
end_idx = end_substr(raw_yaml, "aprs.fi:")
|
|
||||||
if end_idx != -1:
|
|
||||||
# lets insert a comment
|
|
||||||
raw_yaml = insert_str(
|
|
||||||
raw_yaml,
|
|
||||||
"\n # Get the apiKey from your aprs.fi account here: "
|
|
||||||
"\n # http://aprs.fi/account",
|
|
||||||
end_idx,
|
|
||||||
)
|
|
||||||
|
|
||||||
end_idx = end_substr(raw_yaml, "opencagedata:")
|
|
||||||
if end_idx != -1:
|
|
||||||
# lets insert a comment
|
|
||||||
raw_yaml = insert_str(
|
|
||||||
raw_yaml,
|
|
||||||
"\n # (Optional for TimeOpenCageDataPlugin) "
|
|
||||||
"\n # Get the apiKey from your opencagedata account here: "
|
|
||||||
"\n # https://opencagedata.com/dashboard#api-keys",
|
|
||||||
end_idx,
|
|
||||||
)
|
|
||||||
|
|
||||||
end_idx = end_substr(raw_yaml, "openweathermap:")
|
|
||||||
if end_idx != -1:
|
|
||||||
# lets insert a comment
|
|
||||||
raw_yaml = insert_str(
|
|
||||||
raw_yaml,
|
|
||||||
"\n # (Optional for OWMWeatherPlugin) "
|
|
||||||
"\n # Get the apiKey from your "
|
|
||||||
"\n # openweathermap account here: "
|
|
||||||
"\n # https://home.openweathermap.org/api_keys",
|
|
||||||
end_idx,
|
|
||||||
)
|
|
||||||
|
|
||||||
end_idx = end_substr(raw_yaml, "avwx:")
|
|
||||||
if end_idx != -1:
|
|
||||||
# lets insert a comment
|
|
||||||
raw_yaml = insert_str(
|
|
||||||
raw_yaml,
|
|
||||||
"\n # (Optional for AVWXWeatherPlugin) "
|
|
||||||
"\n # Use hosted avwx-api here: https://avwx.rest "
|
|
||||||
"\n # or deploy your own from here: "
|
|
||||||
"\n # https://github.com/avwx-rest/avwx-api",
|
|
||||||
end_idx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return raw_yaml
|
|
||||||
|
|
||||||
|
|
||||||
def create_default_config():
|
|
||||||
"""Create a default config file."""
|
|
||||||
# make sure the directory location exists
|
|
||||||
config_file_expanded = os.path.expanduser(DEFAULT_CONFIG_FILE)
|
|
||||||
config_dir = os.path.dirname(config_file_expanded)
|
|
||||||
if not os.path.exists(config_dir):
|
|
||||||
click.echo(f"Config dir '{config_dir}' doesn't exist, creating.")
|
|
||||||
mkdir_p(config_dir)
|
|
||||||
with open(config_file_expanded, "w+") as cf:
|
|
||||||
cf.write(dump_default_cfg())
|
|
||||||
|
|
||||||
|
|
||||||
def get_config(config_file):
|
|
||||||
"""This tries to read the yaml config from <config_file>."""
|
|
||||||
config_file_expanded = os.path.expanduser(config_file)
|
|
||||||
if os.path.exists(config_file_expanded):
|
|
||||||
with open(config_file_expanded) as stream:
|
|
||||||
config = yaml.load(stream, Loader=yaml.FullLoader)
|
|
||||||
return config
|
|
||||||
else:
|
|
||||||
if config_file == DEFAULT_CONFIG_FILE:
|
|
||||||
click.echo(
|
|
||||||
f"{config_file_expanded} is missing, creating config file",
|
|
||||||
)
|
|
||||||
create_default_config()
|
|
||||||
msg = (
|
|
||||||
"Default config file created at {}. Please edit with your "
|
|
||||||
"settings.".format(config_file)
|
|
||||||
)
|
|
||||||
click.echo(msg)
|
|
||||||
else:
|
|
||||||
# The user provided a config file path different from the
|
|
||||||
# Default, so we won't try and create it, just bitch and bail.
|
|
||||||
msg = f"Custom config file '{config_file}' is missing."
|
|
||||||
click.echo(msg)
|
|
||||||
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
|
|
||||||
def conf_option_exists(conf, chain):
|
|
||||||
_key = chain.pop(0)
|
|
||||||
if _key in conf:
|
|
||||||
return conf_option_exists(conf[_key], chain) if chain else conf[_key]
|
|
||||||
|
|
||||||
|
|
||||||
def check_config_option(config, chain, default_fail=None):
|
|
||||||
result = conf_option_exists(config, chain.copy())
|
|
||||||
if result is None:
|
|
||||||
raise Exception(
|
|
||||||
"'{}' was not in config file".format(
|
|
||||||
chain,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if default_fail:
|
|
||||||
if result == default_fail:
|
|
||||||
# We have to fail and bail if the user hasn't edited
|
|
||||||
# this config option.
|
|
||||||
raise Exception(
|
|
||||||
"Config file needs to be edited from provided defaults for {}.".format(
|
|
||||||
chain,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
# This method tries to parse the config yaml file
|
|
||||||
# and consume the settings.
|
|
||||||
# If the required params don't exist,
|
|
||||||
# it will look in the environment
|
|
||||||
def parse_config(config_file):
|
|
||||||
# for now we still use globals....ugh
|
|
||||||
global CONFIG
|
|
||||||
|
|
||||||
def fail(msg):
|
|
||||||
click.echo(msg)
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
def check_option(config, chain, default_fail=None):
|
|
||||||
try:
|
|
||||||
config = check_config_option(config, chain, default_fail=default_fail)
|
|
||||||
except Exception as ex:
|
|
||||||
fail(repr(ex))
|
|
||||||
else:
|
|
||||||
return config
|
|
||||||
|
|
||||||
config = get_config(config_file)
|
|
||||||
|
|
||||||
# special check here to make sure user has edited the config file
|
|
||||||
# and changed the ham callsign
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
[
|
|
||||||
"ham",
|
|
||||||
"callsign",
|
|
||||||
],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["ham"]["callsign"],
|
|
||||||
)
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
["services", "aprs.fi", "apiKey"],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["services"]["aprs.fi"]["apiKey"],
|
|
||||||
)
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
["aprs", "login"],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["login"],
|
|
||||||
)
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
["aprs", "password"],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure they change the admin password
|
|
||||||
if config["aprsd"]["web"]["enabled"] is True:
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
["aprsd", "web", "users", "admin"],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
|
|
||||||
)
|
|
||||||
|
|
||||||
if config["aprsd"]["watch_list"]["enabled"] is True:
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
["aprsd", "watch_list", "alert_callsign"],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["watch_list"]["alert_callsign"],
|
|
||||||
)
|
|
||||||
|
|
||||||
if config["aprsd"]["email"]["enabled"] is True:
|
|
||||||
# Check IMAP server settings
|
|
||||||
check_option(config, ["aprsd", "email", "imap", "host"])
|
|
||||||
check_option(config, ["aprsd", "email", "imap", "port"])
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
["aprsd", "email", "imap", "login"],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["login"],
|
|
||||||
)
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
["aprsd", "email", "imap", "password"],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["imap"]["password"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check SMTP server settings
|
|
||||||
check_option(config, ["aprsd", "email", "smtp", "host"])
|
|
||||||
check_option(config, ["aprsd", "email", "smtp", "port"])
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
["aprsd", "email", "smtp", "login"],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["login"],
|
|
||||||
)
|
|
||||||
check_option(
|
|
||||||
config,
|
|
||||||
["aprsd", "email", "smtp", "password"],
|
|
||||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["email"]["smtp"]["password"],
|
|
||||||
)
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def human_size(bytes, units=None):
|
def human_size(bytes, units=None):
|
||||||
"""Returns a human readable string representation of bytes"""
|
"""Returns a human readable string representation of bytes"""
|
||||||
if not units:
|
if not units:
|
||||||
|
@ -28,7 +28,7 @@ RUN addgroup --gid $GID $APRS_USER
|
|||||||
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
||||||
|
|
||||||
# Install aprsd
|
# Install aprsd
|
||||||
RUN /usr/local/bin/pip3 install aprsd==2.3.0
|
RUN /usr/local/bin/pip3 install aprsd==2.3.1
|
||||||
|
|
||||||
# Ensure /config is there with a default config file
|
# Ensure /config is there with a default config file
|
||||||
USER root
|
USER root
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
FROM python:3.8-slim as aprsd
|
FROM python:3.8-slim as aprsd
|
||||||
|
|
||||||
# Dockerfile for building a container during aprsd development.
|
# Dockerfile for building a container during aprsd development.
|
||||||
ARG BRANCH
|
ARG branch
|
||||||
ARG UID
|
ARG UID
|
||||||
ARG GID
|
ARG GID
|
||||||
|
|
||||||
ENV APRS_USER=aprs
|
ENV APRS_USER=aprs
|
||||||
ENV HOME=/home/aprs
|
ENV HOME=/home/aprs
|
||||||
ENV APRSD=http://github.com/craigerl/aprsd.git
|
ENV APRSD=http://github.com/craigerl/aprsd.git
|
||||||
ENV APRSD_BRANCH=${BRANCH:-master}
|
ENV APRSD_BRANCH=${branch:-master}
|
||||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||||
ENV UID=${UID:-1000}
|
ENV UID=${UID:-1000}
|
||||||
ENV GID=${GID:-1000}
|
ENV GID=${GID:-1000}
|
||||||
|
@ -15,14 +15,18 @@ EOF
|
|||||||
|
|
||||||
ALL_PLATFORMS=0
|
ALL_PLATFORMS=0
|
||||||
DEV=0
|
DEV=0
|
||||||
TAG="master"
|
TAG="latest"
|
||||||
|
BRANCH="master"
|
||||||
|
|
||||||
while getopts “t:da” OPTION
|
while getopts “t:dab:” OPTION
|
||||||
do
|
do
|
||||||
case $OPTION in
|
case $OPTION in
|
||||||
t)
|
t)
|
||||||
TAG=$OPTARG
|
TAG=$OPTARG
|
||||||
;;
|
;;
|
||||||
|
b)
|
||||||
|
BRANCH=$OPTARG
|
||||||
|
;;
|
||||||
a)
|
a)
|
||||||
ALL_PLATFORMS=1
|
ALL_PLATFORMS=1
|
||||||
;;
|
;;
|
||||||
@ -36,7 +40,7 @@ do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
VERSION="2.2.1"
|
VERSION="2.3.1"
|
||||||
|
|
||||||
if [ $ALL_PLATFORMS -eq 1 ]
|
if [ $ALL_PLATFORMS -eq 1 ]
|
||||||
then
|
then
|
||||||
@ -45,20 +49,28 @@ else
|
|||||||
PLATFORMS="linux/amd64"
|
PLATFORMS="linux/amd64"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Build with tag=${TAG} BRANCH=${BRANCH} dev?=${DEV} platforms?=${PLATFORMS}"
|
||||||
|
|
||||||
|
|
||||||
|
echo "Destroying old multiarch build container"
|
||||||
|
docker buildx rm multiarch
|
||||||
|
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
|
||||||
|
|
||||||
if [ $DEV -eq 1 ]
|
if [ $DEV -eq 1 ]
|
||||||
then
|
then
|
||||||
|
echo "Build -DEV- with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
|
||||||
# Use this script to locally build the docker image
|
# Use this script to locally build the docker image
|
||||||
docker buildx build --push --platform $PLATFORMS \
|
docker buildx build --push --platform $PLATFORMS \
|
||||||
-t harbor.hemna.com/hemna6969/aprsd:$TAG \
|
-t harbor.hemna.com/hemna6969/aprsd:$TAG \
|
||||||
-f Dockerfile-dev --no-cache .
|
-f Dockerfile-dev --build-arg branch=$BRANCH --no-cache .
|
||||||
else
|
else
|
||||||
# Use this script to locally build the docker image
|
# Use this script to locally build the docker image
|
||||||
|
echo "Build with tag=${TAG} BRANCH=${BRANCH} platforms?=${PLATFORMS}"
|
||||||
docker buildx build --push --platform $PLATFORMS \
|
docker buildx build --push --platform $PLATFORMS \
|
||||||
-t hemna6969/aprsd:$VERSION \
|
-t hemna6969/aprsd:$VERSION \
|
||||||
-t hemna6969/aprsd:latest \
|
-t hemna6969/aprsd:$TAG \
|
||||||
-t harbor.hemna.com/hemna6969/aprsd:latest \
|
-t harbor.hemna.com/hemna6969/aprsd:$TAG \
|
||||||
-t harbor.hemna.com/hemna6969/aprsd:$VERSION \
|
-t harbor.hemna.com/hemna6969/aprsd:$VERSION \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
|
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
@ -5,21 +5,21 @@ from aprsd.plugins import email
|
|||||||
|
|
||||||
class TestEmail(unittest.TestCase):
|
class TestEmail(unittest.TestCase):
|
||||||
def test_get_email_from_shortcut(self):
|
def test_get_email_from_shortcut(self):
|
||||||
email.CONFIG = {"aprsd": {"email": {"shortcuts": {}}}}
|
config = {"aprsd": {"email": {"shortcuts": {}}}}
|
||||||
email_address = "something@something.com"
|
email_address = "something@something.com"
|
||||||
addr = f"-{email_address}"
|
addr = f"-{email_address}"
|
||||||
actual = email.get_email_from_shortcut(addr)
|
actual = email.get_email_from_shortcut(config, addr)
|
||||||
self.assertEqual(addr, actual)
|
self.assertEqual(addr, actual)
|
||||||
|
|
||||||
email.CONFIG = {"aprsd": {"email": {"nothing": "nothing"}}}
|
config = {"aprsd": {"email": {"nothing": "nothing"}}}
|
||||||
actual = email.get_email_from_shortcut(addr)
|
actual = email.get_email_from_shortcut(config, addr)
|
||||||
self.assertEqual(addr, actual)
|
self.assertEqual(addr, actual)
|
||||||
|
|
||||||
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}}
|
config = {"aprsd": {"email": {"shortcuts": {"not_used": "empty"}}}}
|
||||||
actual = email.get_email_from_shortcut(addr)
|
actual = email.get_email_from_shortcut(config, addr)
|
||||||
self.assertEqual(addr, actual)
|
self.assertEqual(addr, actual)
|
||||||
|
|
||||||
email.CONFIG = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}}
|
config = {"aprsd": {"email": {"shortcuts": {"-wb": email_address}}}}
|
||||||
short = "-wb"
|
short = "-wb"
|
||||||
actual = email.get_email_from_shortcut(short)
|
actual = email.get_email_from_shortcut(config, short)
|
||||||
self.assertEqual(email_address, actual)
|
self.assertEqual(email_address, actual)
|
||||||
|
@ -4,7 +4,7 @@ from unittest import mock
|
|||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import messaging, packets, stats, utils
|
from aprsd import config, messaging, packets, stats
|
||||||
from aprsd.fuzzyclock import fuzzy
|
from aprsd.fuzzyclock import fuzzy
|
||||||
from aprsd.plugins import fortune as fortune_plugin
|
from aprsd.plugins import fortune as fortune_plugin
|
||||||
from aprsd.plugins import ping as ping_plugin
|
from aprsd.plugins import ping as ping_plugin
|
||||||
@ -19,7 +19,7 @@ class TestPlugin(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.fromcall = fake.FAKE_FROM_CALLSIGN
|
self.fromcall = fake.FAKE_FROM_CALLSIGN
|
||||||
self.ack = 1
|
self.ack = 1
|
||||||
self.config = utils.DEFAULT_CONFIG_DICT
|
self.config = config.DEFAULT_CONFIG_DICT
|
||||||
self.config["ham"]["callsign"] = self.fromcall
|
self.config["ham"]["callsign"] = self.fromcall
|
||||||
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
|
self.config["aprs"]["login"] = fake.FAKE_TO_CALLSIGN
|
||||||
# Inintialize the stats object with the config
|
# Inintialize the stats object with the config
|
||||||
|
Loading…
x
Reference in New Issue
Block a user