mirror of
https://github.com/craigerl/aprsd.git
synced 2024-09-27 15:46:53 -04:00
Merge pull request #15 from hemna/plugins
Created plugin.py for Command Plugins
This commit is contained in:
commit
ccad85a8bf
163
aprsd/main.py
163
aprsd/main.py
@ -24,7 +24,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import email
|
import email
|
||||||
import imaplib
|
import imaplib
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
@ -33,7 +32,6 @@ import select
|
|||||||
import signal
|
import signal
|
||||||
import smtplib
|
import smtplib
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@ -43,14 +41,12 @@ from logging.handlers import RotatingFileHandler
|
|||||||
import click
|
import click
|
||||||
import click_completion
|
import click_completion
|
||||||
import imapclient
|
import imapclient
|
||||||
import requests
|
|
||||||
import six
|
import six
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import utils
|
from aprsd import plugin, utils
|
||||||
from aprsd.fuzzyclock import fuzzy
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -703,11 +699,6 @@ def sample_config():
|
|||||||
|
|
||||||
COMMAND_ENVELOPE = {
|
COMMAND_ENVELOPE = {
|
||||||
"email": {"command": "^-.*", "function": "command_email"},
|
"email": {"command": "^-.*", "function": "command_email"},
|
||||||
"fortune": {"command": "^[fF]", "function": "command_fortune"},
|
|
||||||
"location": {"command": "^[lL]", "function": "command_location"},
|
|
||||||
"weather": {"command": "^[wW]", "function": "command_weather"},
|
|
||||||
"ping": {"command": "^[pP]", "function": "command_ping"},
|
|
||||||
"time": {"command": "^[tT]", "function": "command_time"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -768,149 +759,6 @@ def command_email(fromcall, message, ack):
|
|||||||
return (fromcall, message, ack)
|
return (fromcall, message, ack)
|
||||||
|
|
||||||
|
|
||||||
def command_fortune(fromcall, message, ack):
|
|
||||||
try:
|
|
||||||
process = subprocess.Popen(
|
|
||||||
["/usr/games/fortune", "-s", "-n 60"], stdout=subprocess.PIPE
|
|
||||||
)
|
|
||||||
reply = process.communicate()[0]
|
|
||||||
# send_message(fromcall, reply.rstrip())
|
|
||||||
reply = reply.decode(errors="ignore").rstrip()
|
|
||||||
|
|
||||||
except Exception as ex:
|
|
||||||
reply = "Fortune command failed '{}'".format(ex)
|
|
||||||
LOG.error(reply)
|
|
||||||
|
|
||||||
send_message(fromcall, reply)
|
|
||||||
return (fromcall, message, ack)
|
|
||||||
|
|
||||||
|
|
||||||
def command_location(fromcall, message, ack):
|
|
||||||
LOG.info("Location COMMAND")
|
|
||||||
# get last location of a callsign, get descriptive name from weather service
|
|
||||||
try:
|
|
||||||
a = re.search(
|
|
||||||
r"^.*\s+(.*)", message
|
|
||||||
) # optional second argument is a callsign to search
|
|
||||||
if a is not None:
|
|
||||||
searchcall = a.group(1)
|
|
||||||
searchcall = searchcall.upper()
|
|
||||||
else:
|
|
||||||
searchcall = fromcall # if no second argument, search for calling station
|
|
||||||
url = (
|
|
||||||
"http://api.aprs.fi/api/get?name="
|
|
||||||
+ searchcall
|
|
||||||
+ "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
|
||||||
)
|
|
||||||
response = requests.get(url)
|
|
||||||
# aprs_data = json.loads(response.read())
|
|
||||||
aprs_data = json.loads(response.text)
|
|
||||||
lat = aprs_data["entries"][0]["lat"]
|
|
||||||
lon = aprs_data["entries"][0]["lng"]
|
|
||||||
try: # altitude not always provided
|
|
||||||
alt = aprs_data["entries"][0]["altitude"]
|
|
||||||
except Exception:
|
|
||||||
alt = 0
|
|
||||||
altfeet = int(alt * 3.28084)
|
|
||||||
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
|
||||||
aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
|
||||||
"ascii", errors="ignore"
|
|
||||||
) # unicode to ascii
|
|
||||||
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
|
||||||
delta_hours = delta_seconds / 60 / 60
|
|
||||||
url2 = (
|
|
||||||
"https://forecast.weather.gov/MapClick.php?lat="
|
|
||||||
+ str(lat)
|
|
||||||
+ "&lon="
|
|
||||||
+ str(lon)
|
|
||||||
+ "&FcstType=json"
|
|
||||||
)
|
|
||||||
response2 = requests.get(url2)
|
|
||||||
wx_data = json.loads(response2.text)
|
|
||||||
|
|
||||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
|
||||||
searchcall,
|
|
||||||
wx_data["location"]["areaDescription"],
|
|
||||||
str(altfeet),
|
|
||||||
str(alt),
|
|
||||||
str(lon),
|
|
||||||
str("%.1f" % round(delta_hours, 1)),
|
|
||||||
)
|
|
||||||
# reply = reply.encode('ascii', errors='ignore') # unicode to ascii
|
|
||||||
send_message(fromcall, reply.rstrip())
|
|
||||||
except Exception as e:
|
|
||||||
LOG.debug("Locate failed with: " + "%s" % str(e))
|
|
||||||
reply = "Unable to find station " + searchcall + ". Sending beacons?"
|
|
||||||
send_message(fromcall, reply.rstrip())
|
|
||||||
|
|
||||||
return (fromcall, message, ack)
|
|
||||||
|
|
||||||
|
|
||||||
def command_weather(fromcall, message, ack):
|
|
||||||
"""Do weather command and send response."""
|
|
||||||
LOG.info("WEATHER COMMAND")
|
|
||||||
try:
|
|
||||||
url = (
|
|
||||||
"http://api.aprs.fi/api/get?"
|
|
||||||
"&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
|
||||||
"&name=%s" % fromcall
|
|
||||||
)
|
|
||||||
response = requests.get(url)
|
|
||||||
# aprs_data = json.loads(response.read())
|
|
||||||
aprs_data = json.loads(response.text)
|
|
||||||
lat = aprs_data["entries"][0]["lat"]
|
|
||||||
lon = aprs_data["entries"][0]["lng"]
|
|
||||||
url2 = (
|
|
||||||
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
|
||||||
"&lon=%s&FcstType=json" % (lat, lon)
|
|
||||||
)
|
|
||||||
response2 = requests.get(url2)
|
|
||||||
# wx_data = json.loads(response2.read())
|
|
||||||
wx_data = json.loads(response2.text)
|
|
||||||
reply = "%sF(%sF/%sF) %s. %s, %s." % (
|
|
||||||
wx_data["currentobservation"]["Temp"],
|
|
||||||
wx_data["data"]["temperature"][0],
|
|
||||||
wx_data["data"]["temperature"][1],
|
|
||||||
wx_data["data"]["weather"][0],
|
|
||||||
wx_data["time"]["startPeriodName"][1],
|
|
||||||
wx_data["data"]["weather"][1],
|
|
||||||
)
|
|
||||||
LOG.debug("reply: " + reply.rstrip())
|
|
||||||
send_message(fromcall, reply.rstrip())
|
|
||||||
except Exception as e:
|
|
||||||
LOG.debug("Weather failed with: " + "%s" % str(e))
|
|
||||||
reply = "Unable to find you (send beacon?)"
|
|
||||||
|
|
||||||
return (fromcall, message, ack)
|
|
||||||
|
|
||||||
|
|
||||||
def command_ping(fromcall, message, ack):
|
|
||||||
LOG.info("PING COMMAND")
|
|
||||||
stm = time.localtime()
|
|
||||||
h = stm.tm_hour
|
|
||||||
m = stm.tm_min
|
|
||||||
s = stm.tm_sec
|
|
||||||
reply = "Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
|
|
||||||
send_message(fromcall, reply.rstrip())
|
|
||||||
return (fromcall, message, ack)
|
|
||||||
|
|
||||||
|
|
||||||
def command_time(fromcall, message, ack):
|
|
||||||
LOG.info("TIME COMMAND")
|
|
||||||
stm = time.localtime()
|
|
||||||
h = stm.tm_hour
|
|
||||||
m = stm.tm_min
|
|
||||||
cur_time = fuzzy(h, m, 1)
|
|
||||||
reply = "{} ({}:{} PDT) ({})".format(
|
|
||||||
cur_time, str(h), str(m).rjust(2, "0"), message.rstrip()
|
|
||||||
)
|
|
||||||
thread = threading.Thread(
|
|
||||||
target=send_message, name="send_message", args=(fromcall, reply)
|
|
||||||
)
|
|
||||||
thread.start()
|
|
||||||
return (fromcall, message, ack)
|
|
||||||
|
|
||||||
|
|
||||||
# main() ###
|
# main() ###
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
@ -964,6 +812,9 @@ def server(loglevel, quiet, config_file):
|
|||||||
|
|
||||||
read_sockets = [client_sock]
|
read_sockets = [client_sock]
|
||||||
|
|
||||||
|
# Register plugins
|
||||||
|
pm = plugin.setup_plugins(CONFIG)
|
||||||
|
|
||||||
fromcall = message = ack = None
|
fromcall = message = ack = None
|
||||||
while True:
|
while True:
|
||||||
LOG.debug("Main loop start")
|
LOG.debug("Main loop start")
|
||||||
@ -1038,6 +889,12 @@ def server(loglevel, quiet, config_file):
|
|||||||
ack_dict.update({int(a.group(1)): 1})
|
ack_dict.update({int(a.group(1)): 1})
|
||||||
continue # break out of this so we don't ack an ack at the end
|
continue # break out of this so we don't ack an ack at the end
|
||||||
|
|
||||||
|
# call our `myhook` hook
|
||||||
|
results = pm.hook.run(fromcall=fromcall, message=message, ack=ack)
|
||||||
|
LOG.info("PLUGINS returned {}".format(results))
|
||||||
|
for reply in results:
|
||||||
|
send_message(fromcall, reply)
|
||||||
|
|
||||||
# it's not an ack, so try and process user input
|
# it's not an ack, so try and process user input
|
||||||
found_command = False
|
found_command = False
|
||||||
for key in COMMAND_ENVELOPE:
|
for key in COMMAND_ENVELOPE:
|
||||||
|
331
aprsd/plugin.py
Normal file
331
aprsd/plugin.py
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
# The base plugin class
|
||||||
|
import abc
|
||||||
|
import fnmatch
|
||||||
|
import imp
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pluggy
|
||||||
|
import requests
|
||||||
|
import six
|
||||||
|
|
||||||
|
from aprsd.fuzzyclock import fuzzy
|
||||||
|
|
||||||
|
# setup the global logger
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
hookspec = pluggy.HookspecMarker("aprsd")
|
||||||
|
hookimpl = pluggy.HookimplMarker("aprsd")
|
||||||
|
|
||||||
|
CORE_PLUGINS = [
|
||||||
|
"FortunePlugin",
|
||||||
|
"LocationPlugin",
|
||||||
|
"PingPlugin",
|
||||||
|
"TimePlugin",
|
||||||
|
"WeatherPlugin",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def setup_plugins(config):
|
||||||
|
"""Create the plugin manager and register plugins."""
|
||||||
|
|
||||||
|
LOG.info("Loading Core APRSD Command Plugins")
|
||||||
|
enabled_plugins = config["aprsd"].get("enabled_plugins", None)
|
||||||
|
pm = pluggy.PluginManager("aprsd")
|
||||||
|
pm.add_hookspecs(APRSDCommandSpec)
|
||||||
|
for p_name in CORE_PLUGINS:
|
||||||
|
plugin_obj = None
|
||||||
|
if enabled_plugins:
|
||||||
|
if p_name in enabled_plugins:
|
||||||
|
plugin_obj = globals()[p_name]()
|
||||||
|
else:
|
||||||
|
# Enabled plugins isn't set, so we default to loading all of
|
||||||
|
# the core plugins.
|
||||||
|
plugin_obj = globals()[p_name]()
|
||||||
|
|
||||||
|
if plugin_obj:
|
||||||
|
LOG.info(
|
||||||
|
"Registering Command plugin '{}'({}) '{}'".format(
|
||||||
|
p_name, plugin_obj.version, plugin_obj.command_regex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pm.register(plugin_obj)
|
||||||
|
|
||||||
|
plugin_dir = config["aprsd"].get("plugin_dir", None)
|
||||||
|
if plugin_dir:
|
||||||
|
LOG.info("Trying to load custom plugins from '{}'".format(plugin_dir))
|
||||||
|
cpm = PluginManager()
|
||||||
|
plugins_list = cpm.load_plugins(plugin_dir)
|
||||||
|
LOG.info("Discovered {} modules to load".format(len(plugins_list)))
|
||||||
|
for o in plugins_list:
|
||||||
|
plugin_obj = None
|
||||||
|
if enabled_plugins:
|
||||||
|
if o["name"] in enabled_plugins:
|
||||||
|
plugin_obj = o["obj"]
|
||||||
|
else:
|
||||||
|
LOG.info(
|
||||||
|
"'{}' plugin not listed in config aprsd:enabled_plugins".format(
|
||||||
|
o["name"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# not setting enabled plugins means load all?
|
||||||
|
plugin_obj = o["obj"]
|
||||||
|
|
||||||
|
if plugin_obj:
|
||||||
|
LOG.info(
|
||||||
|
"Registering Command plugin '{}'({}) '{}'".format(
|
||||||
|
o["name"], o["obj"].version, o["obj"].command_regex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pm.register(o["obj"])
|
||||||
|
|
||||||
|
else:
|
||||||
|
LOG.info("Skipping Custom Plugins.")
|
||||||
|
|
||||||
|
LOG.info("Completed Plugin Loading.")
|
||||||
|
return pm
|
||||||
|
|
||||||
|
|
||||||
|
class PluginManager(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.obj_list = []
|
||||||
|
|
||||||
|
def load_plugins(self, module_path):
|
||||||
|
dir_path = os.path.dirname(os.path.realpath(module_path))
|
||||||
|
pattern = "*.py"
|
||||||
|
|
||||||
|
self.obj_list = []
|
||||||
|
|
||||||
|
for path, subdirs, files in os.walk(dir_path):
|
||||||
|
for name in files:
|
||||||
|
if fnmatch.fnmatch(name, pattern):
|
||||||
|
found_module = imp.find_module(name[:-3], [path])
|
||||||
|
module = imp.load_module(
|
||||||
|
name, found_module[0], found_module[1], found_module[2]
|
||||||
|
)
|
||||||
|
for mem_name, obj in inspect.getmembers(module):
|
||||||
|
if (
|
||||||
|
inspect.isclass(obj)
|
||||||
|
and inspect.getmodule(obj) is module
|
||||||
|
and self.is_plugin(obj)
|
||||||
|
):
|
||||||
|
self.obj_list.append({"name": mem_name, "obj": obj()})
|
||||||
|
|
||||||
|
return self.obj_list
|
||||||
|
|
||||||
|
def is_plugin(self, obj):
|
||||||
|
for c in inspect.getmro(obj):
|
||||||
|
if issubclass(c, APRSDPluginBase):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDCommandSpec:
|
||||||
|
"""A hook specification namespace."""
|
||||||
|
|
||||||
|
@hookspec
|
||||||
|
def run(self, fromcall, message, ack):
|
||||||
|
"""My special little hook that you can customize."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class APRSDPluginBase(object):
|
||||||
|
@property
|
||||||
|
def command_regex(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def run(self, fromcall, message, ack):
|
||||||
|
if re.search(self.command_regex, message):
|
||||||
|
return self.command(fromcall, message, ack)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
"""This is the command that runs when the regex matches.
|
||||||
|
|
||||||
|
To reply with a message over the air, return a string
|
||||||
|
to send.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FortunePlugin(APRSDPluginBase):
|
||||||
|
"""Fortune."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[fF]"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("FortunePlugin")
|
||||||
|
reply = None
|
||||||
|
try:
|
||||||
|
process = subprocess.Popen(
|
||||||
|
["/usr/games/fortune", "-s", "-n 60"], stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
reply = process.communicate()[0]
|
||||||
|
# send_message(fromcall, reply.rstrip())
|
||||||
|
reply = reply.decode(errors="ignore").rstrip()
|
||||||
|
except Exception as ex:
|
||||||
|
reply = "Fortune command failed '{}'".format(ex)
|
||||||
|
LOG.error(reply)
|
||||||
|
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
class LocationPlugin(APRSDPluginBase):
|
||||||
|
"""Location!"""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[lL]"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("Location Plugin")
|
||||||
|
# get last location of a callsign, get descriptive name from weather service
|
||||||
|
try:
|
||||||
|
a = re.search(
|
||||||
|
r"^.*\s+(.*)", message
|
||||||
|
) # optional second argument is a callsign to search
|
||||||
|
if a is not None:
|
||||||
|
searchcall = a.group(1)
|
||||||
|
searchcall = searchcall.upper()
|
||||||
|
else:
|
||||||
|
searchcall = (
|
||||||
|
fromcall # if no second argument, search for calling station
|
||||||
|
)
|
||||||
|
url = (
|
||||||
|
"http://api.aprs.fi/api/get?name="
|
||||||
|
+ searchcall
|
||||||
|
+ "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
||||||
|
)
|
||||||
|
response = requests.get(url)
|
||||||
|
# aprs_data = json.loads(response.read())
|
||||||
|
aprs_data = json.loads(response.text)
|
||||||
|
lat = aprs_data["entries"][0]["lat"]
|
||||||
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
|
try: # altitude not always provided
|
||||||
|
alt = aprs_data["entries"][0]["altitude"]
|
||||||
|
except Exception:
|
||||||
|
alt = 0
|
||||||
|
altfeet = int(alt * 3.28084)
|
||||||
|
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
|
||||||
|
aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
|
||||||
|
"ascii", errors="ignore"
|
||||||
|
) # unicode to ascii
|
||||||
|
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
||||||
|
delta_hours = delta_seconds / 60 / 60
|
||||||
|
url2 = (
|
||||||
|
"https://forecast.weather.gov/MapClick.php?lat="
|
||||||
|
+ str(lat)
|
||||||
|
+ "&lon="
|
||||||
|
+ str(lon)
|
||||||
|
+ "&FcstType=json"
|
||||||
|
)
|
||||||
|
response2 = requests.get(url2)
|
||||||
|
wx_data = json.loads(response2.text)
|
||||||
|
|
||||||
|
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||||
|
searchcall,
|
||||||
|
wx_data["location"]["areaDescription"],
|
||||||
|
str(altfeet),
|
||||||
|
str(alt),
|
||||||
|
str(lon),
|
||||||
|
str("%.1f" % round(delta_hours, 1)),
|
||||||
|
).rstrip()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug("Locate failed with: " + "%s" % str(e))
|
||||||
|
reply = "Unable to find station " + searchcall + ". Sending beacons?"
|
||||||
|
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
class PingPlugin(APRSDPluginBase):
|
||||||
|
"""Ping."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[pP]"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("PINGPlugin")
|
||||||
|
stm = time.localtime()
|
||||||
|
h = stm.tm_hour
|
||||||
|
m = stm.tm_min
|
||||||
|
s = stm.tm_sec
|
||||||
|
reply = (
|
||||||
|
"Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
|
||||||
|
)
|
||||||
|
return reply.rstrip()
|
||||||
|
|
||||||
|
|
||||||
|
class TimePlugin(APRSDPluginBase):
|
||||||
|
"""Time command."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[tT]"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("TIME COMMAND")
|
||||||
|
stm = time.localtime()
|
||||||
|
h = stm.tm_hour
|
||||||
|
m = stm.tm_min
|
||||||
|
cur_time = fuzzy(h, m, 1)
|
||||||
|
reply = "{} ({}:{} PDT) ({})".format(
|
||||||
|
cur_time, str(h), str(m).rjust(2, "0"), message.rstrip()
|
||||||
|
)
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherPlugin(APRSDPluginBase):
|
||||||
|
"""Weather Command"""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
command_regex = "^[wW]"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("Weather Plugin")
|
||||||
|
try:
|
||||||
|
url = (
|
||||||
|
"http://api.aprs.fi/api/get?"
|
||||||
|
"&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
|
||||||
|
"&name=%s" % fromcall
|
||||||
|
)
|
||||||
|
response = requests.get(url)
|
||||||
|
# aprs_data = json.loads(response.read())
|
||||||
|
aprs_data = json.loads(response.text)
|
||||||
|
lat = aprs_data["entries"][0]["lat"]
|
||||||
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
|
url2 = (
|
||||||
|
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||||
|
"&lon=%s&FcstType=json" % (lat, lon)
|
||||||
|
)
|
||||||
|
response2 = requests.get(url2)
|
||||||
|
# wx_data = json.loads(response2.read())
|
||||||
|
wx_data = json.loads(response2.text)
|
||||||
|
reply = (
|
||||||
|
"%sF(%sF/%sF) %s. %s, %s."
|
||||||
|
% (
|
||||||
|
wx_data["currentobservation"]["Temp"],
|
||||||
|
wx_data["data"]["temperature"][0],
|
||||||
|
wx_data["data"]["temperature"][1],
|
||||||
|
wx_data["data"]["weather"][0],
|
||||||
|
wx_data["time"]["startPeriodName"][1],
|
||||||
|
wx_data["data"]["weather"][1],
|
||||||
|
).rstrip()
|
||||||
|
)
|
||||||
|
LOG.debug("reply: '{}' ".format(reply))
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug("Weather failed with: " + "%s" % str(e))
|
||||||
|
reply = "Unable to find you (send beacon?)"
|
||||||
|
|
||||||
|
return reply
|
@ -7,6 +7,8 @@ import sys
|
|||||||
import click
|
import click
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from aprsd import plugin
|
||||||
|
|
||||||
# an example of what should be in the ~/.aprsd/config.yml
|
# an example of what should be in the ~/.aprsd/config.yml
|
||||||
DEFAULT_CONFIG_DICT = {
|
DEFAULT_CONFIG_DICT = {
|
||||||
"ham": {"callsign": "KFART"},
|
"ham": {"callsign": "KFART"},
|
||||||
@ -36,6 +38,10 @@ DEFAULT_CONFIG_DICT = {
|
|||||||
"port": 993,
|
"port": 993,
|
||||||
"use_ssl": True,
|
"use_ssl": True,
|
||||||
},
|
},
|
||||||
|
"aprsd": {
|
||||||
|
"plugin_dir": "~/.config/aprsd/plugins",
|
||||||
|
"enabled_plugins": plugin.CORE_PLUGINS,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_CONFIG_FILE = "~/.config/aprsd/aprsd.yml"
|
DEFAULT_CONFIG_FILE = "~/.config/aprsd/aprsd.yml"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
aprsd:
|
aprsd:
|
||||||
image: hemna6969/aprsd:latest
|
image: hemna6969/aprsd:latest
|
||||||
container_name: aprsd
|
container_name: aprsd
|
||||||
volumes:
|
volumes:
|
||||||
- /opt/docker/aprsd/config:/config
|
- /opt/docker/aprsd/config:/config
|
||||||
|
0
examples/plugins/__init__.py
Normal file
0
examples/plugins/__init__.py
Normal file
18
examples/plugins/example_plugin.py
Normal file
18
examples/plugins/example_plugin.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from aprsd import plugin
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class HelloPlugin(plugin.APRSDPluginBase):
|
||||||
|
"""Hello World."""
|
||||||
|
|
||||||
|
version = "1.0"
|
||||||
|
# matches any string starting with h or H
|
||||||
|
command_regex = "^[hH]"
|
||||||
|
|
||||||
|
def command(self, fromcall, message, ack):
|
||||||
|
LOG.info("HelloPlugin")
|
||||||
|
reply = "Hello '{}'".format(fromcall)
|
||||||
|
return reply
|
@ -1,6 +1,7 @@
|
|||||||
click
|
click
|
||||||
click-completion
|
click-completion
|
||||||
imapclient
|
imapclient
|
||||||
|
pluggy
|
||||||
pbr
|
pbr
|
||||||
pyyaml
|
pyyaml
|
||||||
six
|
six
|
||||||
|
2
tox.ini
2
tox.ini
@ -21,7 +21,7 @@ commands =
|
|||||||
# Use -Werror to treat warnings as errors.
|
# Use -Werror to treat warnings as errors.
|
||||||
# {envpython} -bb -Werror -m pytest \
|
# {envpython} -bb -Werror -m pytest \
|
||||||
# --cov="{envsitepackagesdir}/aprsd" --cov-report=html --cov-report=term {posargs}
|
# --cov="{envsitepackagesdir}/aprsd" --cov-report=html --cov-report=term {posargs}
|
||||||
{envpython} -bb -Werror -m pytest {posargs}
|
{envpython} -bb -m pytest {posargs}
|
||||||
|
|
||||||
[testenv:py27]
|
[testenv:py27]
|
||||||
setenv = VIRTUAL_ENV={envdir}
|
setenv = VIRTUAL_ENV={envdir}
|
||||||
|
Loading…
Reference in New Issue
Block a user