mirror of
https://github.com/craigerl/aprsd.git
synced 2024-11-17 22:01:49 -05:00
Merge pull request #40 from craigerl/new_plugins
Added new time plugins
This commit is contained in:
commit
f8c001dc49
@ -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
|
||||
|
@ -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
72
aprsd/plugin_utils.py
Normal 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)
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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,24 +174,12 @@ def get_config(config_file):
|
||||
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):
|
||||
# for now we still use globals....ugh
|
||||
global CONFIG
|
||||
|
||||
def fail(msg):
|
||||
click.echo(msg)
|
||||
sys.exit(-1)
|
||||
|
||||
def check_option(config, section, name=None, default=None, default_fail=None):
|
||||
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:
|
||||
fail(
|
||||
raise Exception(
|
||||
"'{}' was not in '{}' section of config file".format(
|
||||
name,
|
||||
section,
|
||||
@ -187,9 +195,32 @@ def parse_config(config_file):
|
||||
):
|
||||
# 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.")
|
||||
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,
|
||||
# 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, section, name=None, default=None, default_fail=None):
|
||||
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
|
||||
|
||||
config = get_config(config_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
|
||||
|
@ -11,3 +11,4 @@ aprslib
|
||||
py3-validate-email
|
||||
pre-commit
|
||||
pytz
|
||||
opencage
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user