Merge pull request #41 from craigerl/openweathermap

Added openweathermap weather plugin
This commit is contained in:
Walter A. Boring IV 2021-01-21 10:10:26 -05:00 committed by GitHub
commit 2f7c1bfcc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 393 additions and 159 deletions

View File

@ -11,6 +11,8 @@ ENV VIRTUAL_ENV=$HOME/.venv3
ENV INSTALL=$HOME/install ENV INSTALL=$HOME/install
RUN apk add --update git vim wget py3-pip py3-virtualenv bash fortune RUN apk add --update git vim wget py3-pip py3-virtualenv bash fortune
RUN apk add --update gcc python3-dev linux-headers musl-dev libffi-dev libc-dev
RUN apk add --update openssl-dev
# Setup Timezone # Setup Timezone
ENV TZ=US/Eastern ENV TZ=US/Eastern

View File

@ -23,8 +23,7 @@ CORE_PLUGINS = [
"aprsd.plugins.ping.PingPlugin", "aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.query.QueryPlugin", "aprsd.plugins.query.QueryPlugin",
"aprsd.plugins.time.TimePlugin", "aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.WeatherPlugin", "aprsd.plugins.weather.USWeatherPlugin",
"aprsd.plugins.weather.WxPlugin",
"aprsd.plugins.version.VersionPlugin", "aprsd.plugins.version.VersionPlugin",
] ]

View File

@ -54,14 +54,20 @@ def get_weather_gov_metar(station):
return json.loads(response) return json.loads(response)
def fetch_openweathermap(api_key, lat, lon, exclude=None): def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
LOG.debug("Fetch openweathermap for {}, {}".format(lat, lon)) LOG.debug("Fetch openweathermap for {}, {}".format(lat, lon))
if not exclude: if not exclude:
exclude = "minutely,hourly,daily,alerts" exclude = "minutely,hourly,daily,alerts"
try: try:
url = ( url = (
"https://api.openweathermap.org/data/2.5/onecall?" "https://api.openweathermap.org/data/2.5/onecall?"
"lat={}&lon={}&appid={}&exclude={}".format(lat, lon, api_key, exclude) "lat={}&lon={}&appid={}&units={}&exclude={}".format(
lat,
lon,
api_key,
units,
exclude,
)
) )
response = requests.get(url) response = requests.get(url)
except Exception as e: except Exception as e:

View File

@ -24,53 +24,53 @@ class LocationPlugin(plugin.APRSDPluginBase):
return "No aprs.fi apikey found" return "No aprs.fi apikey found"
api_key = self.config["aprs.fi"]["apiKey"] api_key = self.config["aprs.fi"]["apiKey"]
# optional second argument is a callsign to search
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
else:
# if no second argument, search for calling station
searchcall = fromcall
try: try:
# optional second argument is a callsign to search aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
a = re.search(r"^.*\s+(.*)", message) except Exception as ex:
if a is not None: LOG.error("Failed to fetch aprs.fi '{}'".format(ex))
searchcall = a.group(1) return "Failed to fetch aprs.fi location"
searchcall = searchcall.upper()
else:
# if no second argument, search for calling station
searchcall = fromcall
try: LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall) if not len(aprs_data["entries"]):
except Exception as ex: LOG.error("Didn't get any entries from aprs.fi")
LOG.error("Failed to fetch aprs.fi '{}'".format(ex)) return "Failed to fetch aprs.fi location"
return "Failed to fetch aprs.fi location"
LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data)) lat = aprs_data["entries"][0]["lat"]
lat = aprs_data["entries"][0]["lat"] lon = aprs_data["entries"][0]["lng"]
lon = aprs_data["entries"][0]["lng"] try: # altitude not always provided
try: # altitude not always provided alt = aprs_data["entries"][0]["altitude"]
alt = aprs_data["entries"][0]["altitude"] except Exception:
except Exception: alt = 0
alt = 0 altfeet = int(alt * 3.28084)
altfeet = int(alt * 3.28084) aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"]
aprs_lasttime_seconds = aprs_data["entries"][0]["lasttime"] # aprs_lasttime_seconds = aprs_lasttime_seconds.encode(
# aprs_lasttime_seconds = aprs_lasttime_seconds.encode( # "ascii", errors="ignore"
# "ascii", errors="ignore" # ) # unicode to ascii
# ) # unicode to ascii delta_seconds = time.time() - int(aprs_lasttime_seconds)
delta_seconds = time.time() - int(aprs_lasttime_seconds) delta_hours = delta_seconds / 60 / 60
delta_hours = delta_seconds / 60 / 60
try: try:
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon) wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
except Exception as ex: except Exception as ex:
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex)) LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
wx_data["location"]["areaDescription"] = "Unkown Location" wx_data = {"location": {"areaDescription": "Unknown Location"}}
reply = "{}: {} {}' {},{} {}h ago".format( reply = "{}: {} {}' {},{} {}h ago".format(
searchcall, searchcall,
wx_data["location"]["areaDescription"], wx_data["location"]["areaDescription"],
str(altfeet), str(altfeet),
str(lat), str(lat),
str(lon), str(lon),
str("%.1f" % round(delta_hours, 1)), str("%.1f" % round(delta_hours, 1)),
).rstrip() ).rstrip()
except Exception as e:
LOG.debug("Locate failed with: " + "%s" % str(e))
reply = "Unable to find station " + searchcall + ". Sending beacons?"
return reply return reply

View File

@ -2,14 +2,24 @@ import json
import logging import logging
import re import re
from aprsd import plugin from aprsd import plugin, plugin_utils, utils
import requests import requests
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
class WeatherPlugin(plugin.APRSDPluginBase): class USWeatherPlugin(plugin.APRSDPluginBase):
"""Weather Command""" """USWeather Command
Returns a weather report for the calling weather station
inside the United States only. This uses the
forecast.weather.gov API to fetch the weather.
This service does not require an apiKey.
How to Call: Send a message to aprsd
"weather" - returns weather near the calling callsign
"""
version = "1.0" version = "1.0"
command_regex = "^[wW]" command_regex = "^[wW]"
@ -19,91 +29,54 @@ class WeatherPlugin(plugin.APRSDPluginBase):
LOG.info("Weather Plugin") LOG.info("Weather Plugin")
api_key = self.config["aprs.fi"]["apiKey"] api_key = self.config["aprs.fi"]["apiKey"]
try: try:
url = ( aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
"http://api.aprs.fi/api/get?" except Exception as ex:
"&what=loc&apikey={}&format=json" LOG.error("Failed to fetch aprs.fi data {}".format(ex))
"&name={}".format(api_key, fromcall) return "Failed to fetch location"
)
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?)"
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
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))
return "Unable to get weather"
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))
return reply return reply
class WxPlugin(plugin.APRSDPluginBase): class USMetarPlugin(plugin.APRSDPluginBase):
"""METAR Command""" """METAR Command
This provides a METAR weather report from a station near the caller
or callsign using the forecast.weather.gov api. This only works
for stations inside the United States.
This service does not require an apiKey.
How to Call: Send a message to aprsd
"metar" - returns metar report near the calling callsign
"metar CALLSIGN" - returns metar report near CALLSIGN
"""
version = "1.0" version = "1.0"
command_regex = "^[wx]" command_regex = "^[metar]"
command_name = "wx (Metar)" command_name = "Metar"
def get_aprs(self, fromcall):
LOG.debug("Fetch aprs.fi location for '{}'".format(fromcall))
api_key = self.config["aprs.fi"]["apiKey"]
try:
url = (
"http://api.aprs.fi/api/get?"
"&what=loc&apikey={}&format=json"
"&name={}".format(api_key, fromcall)
)
response = requests.get(url)
except Exception:
raise Exception("Failed to get aprs.fi location")
else:
response.raise_for_status()
return response
def get_station(self, 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)
)
response = requests.get(url2)
except Exception:
raise Exception("Failed to get metar station")
else:
response.raise_for_status()
return response
def get_metar(self, 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 response
def command(self, fromcall, message, ack): def command(self, fromcall, message, ack):
LOG.info("WX Plugin '{}'".format(message)) LOG.info("WX Plugin '{}'".format(message))
@ -112,7 +85,7 @@ class WxPlugin(plugin.APRSDPluginBase):
searchcall = a.group(1) searchcall = a.group(1)
station = searchcall.upper() station = searchcall.upper()
try: try:
resp = self.get_metar(station) resp = plugin_utils.get_weather_gov_metar(station)
except Exception as e: except Exception as e:
LOG.debug("Weather failed with: {}".format(str(e))) LOG.debug("Weather failed with: {}".format(str(e)))
reply = "Unable to find station METAR" reply = "Unable to find station METAR"
@ -124,36 +97,254 @@ class WxPlugin(plugin.APRSDPluginBase):
else: else:
# if no second argument, search for calling station # if no second argument, search for calling station
fromcall = fromcall fromcall = fromcall
try:
resp = self.get_aprs(fromcall)
except Exception as e:
LOG.debug("Weather failed with: {}".format(str(e)))
reply = "Unable to find you (send beacon?)"
else:
aprs_data = json.loads(resp.text)
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
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))
if not len(aprs_data["entries"]):
LOG.error("Found no entries from aprs.fi!")
return "Failed to fetch location"
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
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))
return "Unable to metar find station."
if wx_data["location"]["metar"]:
station = wx_data["location"]["metar"]
try: try:
resp = self.get_station(lat, lon) resp = plugin_utils.get_weather_gov_metar(station)
except Exception as e: except Exception as e:
LOG.debug("Weather failed with: {}".format(str(e))) LOG.debug("Weather failed with: {}".format(str(e)))
reply = "Unable to find you (send beacon?)" reply = "Failed to get Metar"
else: else:
wx_data = json.loads(resp.text) station_data = json.loads(resp.text)
reply = station_data["properties"]["rawMessage"]
if wx_data["location"]["metar"]: else:
station = wx_data["location"]["metar"] # Couldn't find a station
try: reply = "No Metar station found"
resp = self.get_metar(station)
except Exception as e:
LOG.debug("Weather failed with: {}".format(str(e)))
reply = "Failed to get Metar"
else:
station_data = json.loads(resp.text)
reply = station_data["properties"]["rawMessage"]
else:
# Couldn't find a station
reply = "No Metar station found"
return reply return reply
class OWMWeatherPlugin(plugin.APRSDPluginBase):
"""OpenWeatherMap Weather Command
This provides weather near the caller or callsign.
How to Call: Send a message to aprsd
"weather" - returns the weather near the calling callsign
"weather CALLSIGN" - returns the weather near CALLSIGN
This plugin uses the openweathermap API to fetch
location and weather information.
To use this plugin you need to get an openweathermap
account and apikey.
https://home.openweathermap.org/api_keys
"""
version = "1.0"
command_regex = "^[wW]"
command_name = "Weather"
def command(self, fromcall, message, ack):
LOG.info("OWMWeather Plugin '{}'".format(message))
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
else:
searchcall = fromcall
api_key = self.config["aprs.fi"]["apiKey"]
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
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))
if not len(aprs_data["entries"]):
LOG.error("Found no entries from aprs.fi!")
return "Failed to fetch location"
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"
try:
utils.check_config_option(self.config, "aprsd", "units")
except Exception:
LOG.debug("Couldn't find untis in aprsd:services:units")
units = "metric"
else:
units = self.config["aprsd"]["units"]
api_key = self.config["openweathermap"]["apiKey"]
try:
wx_data = plugin_utils.fetch_openweathermap(
api_key,
lat,
lon,
units=units,
exclude="minutely,hourly",
)
except Exception as ex:
LOG.error("Couldn't fetch openweathermap api '{}'".format(ex))
# default to UTC
return "Unable to get weather"
if units == "metric":
degree = "C"
else:
degree = "F"
if "wind_gust" in wx_data["current"]:
wind = "{:.0f}@{}G{:.0f}".format(
wx_data["current"]["wind_speed"],
wx_data["current"]["wind_deg"],
wx_data["current"]["wind_gust"],
)
else:
wind = "{:.0f}@{}".format(
wx_data["current"]["wind_speed"],
wx_data["current"]["wind_deg"],
)
# LOG.debug(wx_data["current"])
# LOG.debug(wx_data["daily"])
reply = "{} {:.1f}{}/{:.1f}{} Wind {} {}%".format(
wx_data["current"]["weather"][0]["description"],
wx_data["current"]["temp"],
degree,
wx_data["current"]["dew_point"],
degree,
wind,
wx_data["current"]["humidity"],
)
return reply
class AVWXWeatherPlugin(plugin.APRSDPluginBase):
"""AVWXWeatherMap Weather Command
Fetches a METAR weather report for the nearest
weather station from the callsign
Can be called with:
metar - fetches metar for caller
metar <CALLSIGN> - fetches metar for <CALLSIGN>
This plugin requires the avwx-api service
to provide the metar for a station near
the callsign.
avwx-api is an opensource project that has
a hosted service here: https://avwx.rest/
You can launch your own avwx-api in a container
by cloning the githug repo here: https://github.com/avwx-rest/AVWX-API
Then build the docker container with:
docker build -f Dockerfile -t avwx-api:master .
"""
version = "1.0"
command_regex = "^[metar]"
command_name = "Weather"
def command(self, fromcall, message, ack):
LOG.info("OWMWeather Plugin '{}'".format(message))
a = re.search(r"^.*\s+(.*)", message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
else:
searchcall = fromcall
api_key = self.config["aprs.fi"]["apiKey"]
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
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))
if not len(aprs_data["entries"]):
LOG.error("Found no entries from aprs.fi!")
return "Failed to fetch location"
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
try:
utils.check_config_option(self.config, "avwx", "apiKey")
except Exception as ex:
LOG.error("Failed to find config avwx:apiKey {}".format(ex))
return "No avwx apiKey found"
try:
utils.check_config_option(self.config, "avwx", "base_url")
except Exception as ex:
LOG.debut("Didn't find avwx:base_url {}".format(ex))
base_url = "https://avwx.rest"
else:
base_url = self.config["avwx"]["base_url"]
api_key = self.config["avwx"]["apiKey"]
token = "TOKEN {}".format(api_key)
headers = {"Authorization": token}
try:
coord = "{},{}".format(lat, lon)
url = (
"{}/api/station/near/{}?"
"n=1&airport=false&reporting=true&format=json".format(base_url, coord)
)
LOG.debug("Get stations near me '{}'".format(url))
response = requests.get(url, headers=headers)
except Exception as ex:
LOG.error(ex)
raise Exception("Failed to get the weather '{}'".format(ex))
else:
wx_data = json.loads(response.text)
# LOG.debug(wx_data)
station = wx_data[0]["station"]["icao"]
try:
url = (
"{}/api/metar/{}?options=info,translate,summary"
"&airport=true&reporting=true&format=json&onfail=cache".format(
base_url,
station,
)
)
LOG.debug("Get METAR '{}'".format(url))
response = requests.get(url, headers=headers)
except Exception as ex:
LOG.error(ex)
raise Exception("Failed to get metar {}".format(ex))
else:
metar_data = json.loads(response.text)
# LOG.debug(metar_data)
return metar_data["raw"]

View File

@ -24,6 +24,7 @@ DEFAULT_CONFIG_DICT = {
"aprs.fi": {"apiKey": "set me"}, "aprs.fi": {"apiKey": "set me"},
"openweathermap": {"apiKey": "set me"}, "openweathermap": {"apiKey": "set me"},
"opencagedata": {"apiKey": "set me"}, "opencagedata": {"apiKey": "set me"},
"avwx": {"base_url": "http://host:port", "apiKey": "set me"},
"shortcuts": { "shortcuts": {
"aa": "5551239999@vtext.com", "aa": "5551239999@vtext.com",
"cl": "craiglamparter@somedomain.org", "cl": "craiglamparter@somedomain.org",
@ -46,6 +47,7 @@ DEFAULT_CONFIG_DICT = {
"aprsd": { "aprsd": {
"plugin_dir": "~/.config/aprsd/plugins", "plugin_dir": "~/.config/aprsd/plugins",
"enabled_plugins": plugin.CORE_PLUGINS, "enabled_plugins": plugin.CORE_PLUGINS,
"units": "imperial",
}, },
} }
@ -109,7 +111,8 @@ def add_config_comments(raw_yaml):
# lets insert a comment # lets insert a comment
raw_yaml = insert_str( raw_yaml = insert_str(
raw_yaml, raw_yaml,
"\n # Get the apiKey from your aprs.fi account here: http://aprs.fi/account", "\n # Get the apiKey from your aprs.fi account here: "
"\n # http://aprs.fi/account",
end_idx, end_idx,
) )
@ -118,7 +121,9 @@ def add_config_comments(raw_yaml):
# lets insert a comment # lets insert a comment
raw_yaml = insert_str( raw_yaml = insert_str(
raw_yaml, raw_yaml,
"\n # Get the apiKey from your opencagedata account here: https://opencagedata.com/dashboard#api-keys", "\n # (Optional for TimeOpenCageDataPlugin) "
"\n # Get the apiKey from your opencagedata account here: "
"\n # https://opencagedata.com/dashboard#api-keys",
end_idx, end_idx,
) )
@ -127,7 +132,22 @@ def add_config_comments(raw_yaml):
# lets insert a comment # lets insert a comment
raw_yaml = insert_str( raw_yaml = insert_str(
raw_yaml, raw_yaml,
"\n # Get the apiKey from your openweathermap account here: https://home.openweathermap.org/api_keys", "\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, end_idx,
) )

View File

@ -20,6 +20,14 @@ aprsd.client module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
aprsd.dev module
----------------
.. automodule:: aprsd.dev
:members:
:undoc-members:
:show-inheritance:
aprsd.email module aprsd.email module
------------------ ------------------
@ -68,6 +76,14 @@ aprsd.plugin module
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
aprsd.plugin\_utils module
--------------------------
.. automodule:: aprsd.plugin_utils
:members:
:undoc-members:
:show-inheritance:
aprsd.threads module aprsd.threads module
-------------------- --------------------