Merge pull request #40 from craigerl/new_plugins

Added new time plugins
This commit is contained in:
Craig Lamparter 2021-01-20 08:26:48 -08:00 committed by GitHub
commit f8c001dc49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 262 additions and 67 deletions

View File

@ -10,9 +10,9 @@ RUN apk add --update git wget py3-pip py3-virtualenv bash fortune
# Setup Timezone
ENV TZ=US/Eastern
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get install -y tzdata
RUN dpkg-reconfigure --frontend noninteractive tzdata
#RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
#RUN apt-get install -y tzdata
#RUN dpkg-reconfigure --frontend noninteractive tzdata
RUN addgroup --gid 1000 $APRS_USER

View File

@ -2,11 +2,11 @@ FROM alpine:latest as aprsd
# Dockerfile for building a container during aprsd development.
ENV VERSION=1.0.0
ENV VERSION=1.5.1
ENV APRS_USER=aprs
ENV HOME=/home/aprs
ENV APRSD=http://github.com/craigerl/aprsd.git
ENV APRSD_BRANCH="v1.1.0"
ENV APRSD_BRANCH="master"
ENV VIRTUAL_ENV=$HOME/.venv3
ENV INSTALL=$HOME/install

72
aprsd/plugin_utils.py Normal file
View File

@ -0,0 +1,72 @@
# Utilities for plugins to use
import json
import logging
import requests
LOG = logging.getLogger("APRSD")
def get_aprs_fi(api_key, callsign):
LOG.debug("Fetch aprs.fi location for '{}'".format(callsign))
try:
url = (
"http://api.aprs.fi/api/get?"
"&what=loc&apikey={}&format=json"
"&name={}".format(api_key, callsign)
)
response = requests.get(url)
except Exception:
raise Exception("Failed to get aprs.fi location")
else:
response.raise_for_status()
return json.loads(response.text)
def get_weather_gov_for_gps(lat, lon):
LOG.debug("Fetch station at {}, {}".format(lat, lon))
try:
url2 = (
"https://forecast.weather.gov/MapClick.php?lat=%s"
"&lon=%s&FcstType=json" % (lat, lon)
)
LOG.debug("Fetching weather '{}'".format(url2))
response = requests.get(url2)
except Exception as e:
LOG.error(e)
raise Exception("Failed to get weather")
else:
response.raise_for_status()
return json.loads(response.text)
def get_weather_gov_metar(station):
LOG.debug("Fetch metar for station '{}'".format(station))
try:
url = "https://api.weather.gov/stations/{}/observations/latest".format(
station,
)
response = requests.get(url)
except Exception:
raise Exception("Failed to fetch metar")
else:
response.raise_for_status()
return json.loads(response)
def fetch_openweathermap(api_key, lat, lon, exclude=None):
LOG.debug("Fetch openweathermap for {}, {}".format(lat, lon))
if not exclude:
exclude = "minutely,hourly,daily,alerts"
try:
url = (
"https://api.openweathermap.org/data/2.5/onecall?"
"lat={}&lon={}&appid={}&exclude={}".format(lat, lon, api_key, exclude)
)
response = requests.get(url)
except Exception as e:
LOG.error(e)
raise Exception("Failed to get weather")
else:
response.raise_for_status()
return json.loads(response.text)

View File

@ -1,10 +1,8 @@
import json
import logging
import re
import time
from aprsd import plugin
import requests
from aprsd import plugin, plugin_utils, utils
LOG = logging.getLogger("APRSD")
@ -16,11 +14,15 @@ class LocationPlugin(plugin.APRSDPluginBase):
command_regex = "^[lL]"
command_name = "location"
config_items = {"apikey": "aprs.fi api key here"}
def command(self, fromcall, message, ack):
LOG.info("Location Plugin")
# get last location of a callsign, get descriptive name from weather service
try:
utils.check_config_option(self.config, "aprs.fi", "apiKey")
except Exception as ex:
LOG.error("Failed to find config aprs.fi:apikey {}".format(ex))
return "No aprs.fi apikey found"
api_key = self.config["aprs.fi"]["apiKey"]
try:
# optional second argument is a callsign to search
@ -31,14 +33,13 @@ class LocationPlugin(plugin.APRSDPluginBase):
else:
# if no second argument, search for calling station
searchcall = fromcall
url = (
"http://api.aprs.fi/api/get?name="
+ searchcall
+ "&what=loc&apikey={}&format=json".format(api_key)
)
response = requests.get(url)
# aprs_data = json.loads(response.read())
aprs_data = json.loads(response.text)
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
LOG.error("Failed to fetch aprs.fi '{}'".format(ex))
return "Failed to fetch aprs.fi location"
LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
@ -53,15 +54,12 @@ class LocationPlugin(plugin.APRSDPluginBase):
# ) # 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)
try:
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
except Exception as ex:
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
wx_data["location"]["areaDescription"] = "Unkown Location"
reply = "{}: {} {}' {},{} {}h ago".format(
searchcall,

View File

@ -1,7 +1,8 @@
import logging
import time
from aprsd import fuzzyclock, plugin
from aprsd import fuzzyclock, plugin, plugin_utils, utils
from opencage.geocoder import OpenCageGeocode
import pytz
LOG = logging.getLogger("APRSD")
@ -20,15 +21,7 @@ class TimePlugin(plugin.APRSDPluginBase):
def _get_utcnow(self):
return pytz.datetime.datetime.utcnow()
def command(self, fromcall, message, ack):
LOG.info("TIME COMMAND")
# So we can mock this in unit tests
localzone = self._get_local_tz()
# This is inefficient for now, but this enables
# us to add the ability to provide time in the TZ
# of the caller, if we can get the TZ from their callsign location
# This also accounts for running aprsd in different timezones
def build_date_str(self, localzone):
utcnow = self._get_utcnow()
gmt_t = pytz.utc.localize(utcnow)
local_t = gmt_t.astimezone(localzone)
@ -38,10 +31,94 @@ class TimePlugin(plugin.APRSDPluginBase):
local_min = local_t.strftime("%M")
cur_time = fuzzyclock.fuzzy(int(local_hour), int(local_min), 1)
reply = "{} ({}) ({})".format(
reply = "{} ({})".format(
cur_time,
local_short_str,
message.rstrip(),
)
return reply
def command(self, fromcall, message, ack):
LOG.info("TIME COMMAND")
# So we can mock this in unit tests
localzone = self._get_local_tz()
return self.build_date_str(localzone)
class TimeOpenCageDataPlugin(TimePlugin):
"""geocage based timezone fetching."""
version = "1.0"
command_regex = "^[tT]"
command_name = "Time"
def command(self, fromcall, message, ack):
api_key = self.config["aprs.fi"]["apiKey"]
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
except Exception as ex:
LOG.error("Failed to fetch aprs.fi data {}".format(ex))
return "Failed to fetch location"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
try:
utils.check_config_option(self.config, "opencagedata", "apiKey")
except Exception as ex:
LOG.error("Failed to find config opencage:apiKey {}".format(ex))
return "No opencage apiKey found"
try:
opencage_key = self.config["opencagedata"]["apiKey"]
geocoder = OpenCageGeocode(opencage_key)
results = geocoder.reverse_geocode(lat, lon)
except Exception as ex:
LOG.error("Couldn't fetch opencagedata api '{}'".format(ex))
# Default to UTC instead
localzone = pytz.timezone("UTC")
else:
tzone = results[0]["annotations"]["timezone"]["name"]
localzone = pytz.timezone(tzone)
return self.build_date_str(localzone)
class TimeOWMPlugin(TimePlugin):
"""OpenWeatherMap based timezone fetching."""
version = "1.0"
command_regex = "^[tT]"
command_name = "Time"
def command(self, fromcall, message, ack):
api_key = self.config["aprs.fi"]["apiKey"]
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
except Exception as ex:
LOG.error("Failed to fetch aprs.fi data {}".format(ex))
return "Failed to fetch location"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
try:
utils.check_config_option(self.config, "openweathermap", "apiKey")
except Exception as ex:
LOG.error("Failed to find config openweathermap:apiKey {}".format(ex))
return "No openweathermap apiKey found"
api_key = self.config["openweathermap"]["apiKey"]
try:
results = plugin_utils.fetch_openweathermap(api_key, lat, lon)
except Exception as ex:
LOG.error("Couldn't fetch openweathermap api '{}'".format(ex))
# default to UTC
localzone = pytz.timezone("UTC")
else:
tzone = results["timezone"]
localzone = pytz.timezone(tzone)
return self.build_date_str(localzone)

View File

@ -22,6 +22,8 @@ DEFAULT_CONFIG_DICT = {
"logfile": "/tmp/aprsd.log",
},
"aprs.fi": {"apiKey": "set me"},
"openweathermap": {"apiKey": "set me"},
"opencagedata": {"apiKey": "set me"},
"shortcuts": {
"aa": "5551239999@vtext.com",
"cl": "craiglamparter@somedomain.org",
@ -111,6 +113,24 @@ def add_config_comments(raw_yaml):
end_idx,
)
end_idx = end_substr(raw_yaml, "opencagedata:")
if end_idx != -1:
# lets insert a comment
raw_yaml = insert_str(
raw_yaml,
"\n # Get the apiKey from your opencagedata account here: 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 # Get the apiKey from your openweathermap account here: https://home.openweathermap.org/api_keys",
end_idx,
)
return raw_yaml
@ -154,6 +174,35 @@ def get_config(config_file):
sys.exit(-1)
def check_config_option(config, section, name=None, default=None, default_fail=None):
if section in config:
if name and name not in config[section]:
if not default:
raise Exception(
"'{}' was not in '{}' section of config file".format(
name,
section,
),
)
else:
config[section][name] = default
else:
if (
default_fail
and name in config[section]
and config[section][name] == 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.",
)
else:
raise Exception("'%s' section wasn't in config file" % section)
return config
# This method tries to parse the config yaml file
# and consume the settings.
# If the required params don't exist,
@ -167,30 +216,12 @@ def parse_config(config_file):
sys.exit(-1)
def check_option(config, section, name=None, default=None, default_fail=None):
if section in config:
if name and name not in config[section]:
if not default:
fail(
"'{}' was not in '{}' section of config file".format(
name,
section,
),
)
else:
config[section][name] = default
else:
if (
default_fail
and name in config[section]
and config[section][name] == default_fail
):
# We have to fail and bail if the user hasn't edited
# this config option.
fail("Config file needs to be edited from provided defaults.")
try:
config = check_config_option(config, section, name, default, default_fail)
except Exception as ex:
fail(repr(ex))
else:
fail("'%s' section wasn't in config file" % section)
return config
return config
config = get_config(config_file)
check_option(config, "shortcuts")

View File

@ -23,4 +23,4 @@ if [ ! -e "$APRSD_CONFIG" ]; then
echo "'$APRSD_CONFIG' File does not exist. Creating."
aprsd sample-config > $APRSD_CONFIG
fi
$VIRTUAL_ENV/bin/aprsd server -c $APRSD_CONFIG
$VIRTUAL_ENV/bin/aprsd server -c $APRSD_CONFIG --loglevel DEBUG

View File

@ -11,3 +11,4 @@ aprslib
py3-validate-email
pre-commit
pytz
opencage

View File

@ -2,14 +2,18 @@
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile requirements.in
# pip-compile
#
appdirs==1.4.4
# via virtualenv
aprslib==0.6.47
# via -r requirements.in
backoff==1.10.0
# via opencage
certifi==2020.12.5
# via requests
cffi==1.14.4
# via cryptography
cfgv==3.2.0
# via pre-commit
chardet==4.0.0
@ -20,6 +24,8 @@ click==7.1.2
# via
# -r requirements.in
# click-completion
cryptography==3.3.1
# via pyopenssl
distlib==0.3.1
# via virtualenv
dnspython==2.1.0
@ -42,6 +48,8 @@ markupsafe==1.1.1
# via jinja2
nodeenv==1.5.0
# via pre-commit
opencage==1.2.2
# via -r requirements.in
pbr==5.5.1
# via -r requirements.in
pluggy==0.13.1
@ -50,6 +58,10 @@ pre-commit==2.9.3
# via -r requirements.in
py3-validate-email==0.2.12
# via -r requirements.in
pycparser==2.20
# via cffi
pyopenssl==20.0.1
# via opencage
pytz==2020.5
# via -r requirements.in
pyyaml==5.3.1
@ -57,14 +69,19 @@ pyyaml==5.3.1
# -r requirements.in
# pre-commit
requests==2.25.1
# via -r requirements.in
# via
# -r requirements.in
# opencage
shellingham==1.3.2
# via click-completion
six==1.15.0
# via
# -r requirements.in
# click-completion
# cryptography
# imapclient
# opencage
# pyopenssl
# virtualenv
thesmuggler==1.0.1
# via -r requirements.in

View File

@ -97,10 +97,9 @@ class TestPlugin(unittest.TestCase):
message = "time"
local_short_str = local_t.strftime("%H:%M %Z")
expected = "{} ({}) ({})".format(
expected = "{} ({})".format(
cur_time,
local_short_str,
message.rstrip(),
)
actual = time.run(fromcall, message, ack)
self.assertEqual(expected, actual)