mirror of
https://github.com/craigerl/aprsd.git
synced 2024-11-26 18:08:36 -05:00
Added basic service abstraction for weather
Since there are many weather services that provide an API for fetching weather, and some don't work in other countries, this patch adds a new service abstraction for weather. the user configures which weather service they want to use in the config file, then that service is loaded at start time, and the weather plugin uses the WeatherService object to fetch the weather for a lat,lon combo. The WeatherService itself calls the configured service object that fetches and returns the weather. There is only 1 weather service in this patch, which is the same as it used to be. calling forecast.weather.gov, which is a US government API.
This commit is contained in:
parent
ca05676c98
commit
c20705f426
@ -11,7 +11,7 @@ import sys
|
|||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import client, email, plugin, utils
|
from aprsd import client, email, plugin, service, utils
|
||||||
import click
|
import click
|
||||||
import click_completion
|
import click_completion
|
||||||
|
|
||||||
@ -185,6 +185,7 @@ def test_plugin(
|
|||||||
message = " ".join(message)
|
message = " ".join(message)
|
||||||
LOG.info("P'{}' F'{}' C'{}'".format(plugin_path, fromcall, message))
|
LOG.info("P'{}' F'{}' C'{}'".format(plugin_path, fromcall, message))
|
||||||
client.Client(config)
|
client.Client(config)
|
||||||
|
service.WeatherService(config)
|
||||||
|
|
||||||
pm = plugin.PluginManager(config)
|
pm = plugin.PluginManager(config)
|
||||||
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
|
||||||
|
@ -32,7 +32,7 @@ import time
|
|||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import client, email, messaging, plugin, threads, utils
|
from aprsd import client, email, messaging, plugin, service, threads, utils
|
||||||
import aprslib
|
import aprslib
|
||||||
from aprslib.exceptions import LoginError
|
from aprslib.exceptions import LoginError
|
||||||
import click
|
import click
|
||||||
@ -443,6 +443,9 @@ def server(
|
|||||||
LOG.debug("Loading saved MsgTrack object.")
|
LOG.debug("Loading saved MsgTrack object.")
|
||||||
messaging.MsgTrack().load()
|
messaging.MsgTrack().load()
|
||||||
|
|
||||||
|
LOG.info("Loading weather service")
|
||||||
|
service.WeatherService(config)
|
||||||
|
|
||||||
rx_msg_queue = queue.Queue(maxsize=20)
|
rx_msg_queue = queue.Queue(maxsize=20)
|
||||||
tx_msg_queue = queue.Queue(maxsize=20)
|
tx_msg_queue = queue.Queue(maxsize=20)
|
||||||
msg_queues = {"rx": rx_msg_queue, "tx": tx_msg_queue}
|
msg_queues = {"rx": rx_msg_queue, "tx": tx_msg_queue}
|
||||||
|
@ -60,7 +60,9 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def run(self, fromcall, message, ack):
|
def run(self, fromcall, message, ack):
|
||||||
|
LOG.debug("F({}) M({})".format(fromcall, message))
|
||||||
if re.search(self.command_regex, message):
|
if re.search(self.command_regex, message):
|
||||||
|
LOG.debug("call command F{} M{}".format(fromcall, message))
|
||||||
return self.command(fromcall, message, ack)
|
return self.command(fromcall, message, ack)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
53
aprsd/plugin_utils.py
Normal file
53
aprsd/plugin_utils.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Utilities for plugins to use
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
def get_aprs_fi(api_key, callsign):
|
||||||
|
LOG.info("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 response
|
||||||
|
|
||||||
|
|
||||||
|
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 response
|
||||||
|
|
||||||
|
|
||||||
|
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 response
|
@ -2,8 +2,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from aprsd import plugin
|
from aprsd import plugin, plugin_utils, service
|
||||||
import requests
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
@ -18,101 +17,54 @@ class WeatherPlugin(plugin.APRSDPluginBase):
|
|||||||
def command(self, fromcall, message, ack):
|
def command(self, fromcall, message, ack):
|
||||||
LOG.info("Weather Plugin")
|
LOG.info("Weather Plugin")
|
||||||
api_key = self.config["aprs.fi"]["apiKey"]
|
api_key = self.config["aprs.fi"]["apiKey"]
|
||||||
|
|
||||||
|
# Fetching weather for someone else?
|
||||||
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
|
if a is not None:
|
||||||
|
searchcall = a.group(1)
|
||||||
|
else:
|
||||||
|
searchcall = fromcall
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = (
|
resp = plugin_utils.get_aprs_fi(api_key, searchcall)
|
||||||
"http://api.aprs.fi/api/get?"
|
except Exception as e:
|
||||||
"&what=loc&apikey={}&format=json"
|
LOG.debug("Weather failed with: {}".format(str(e)))
|
||||||
"&name={}".format(api_key, fromcall)
|
reply = "Unable to find you (send beacon?)"
|
||||||
)
|
else:
|
||||||
response = requests.get(url)
|
aprs_data = json.loads(resp.text)
|
||||||
# aprs_data = json.loads(response.read())
|
|
||||||
aprs_data = json.loads(response.text)
|
|
||||||
lat = aprs_data["entries"][0]["lat"]
|
lat = aprs_data["entries"][0]["lat"]
|
||||||
lon = aprs_data["entries"][0]["lng"]
|
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
|
try:
|
||||||
|
wx_service = service.WeatherService(self.config)
|
||||||
|
reply = wx_service.forecast_short(lat, lon)
|
||||||
|
# resp = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.debug("Weather failed with: {}".format(str(e)))
|
||||||
|
return "Unable to Lookup weather"
|
||||||
|
else:
|
||||||
|
# wx_data = json.loads(resp.text)
|
||||||
|
|
||||||
|
LOG.debug("reply: '{}' ".format(reply))
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
class WxPlugin(plugin.APRSDPluginBase):
|
class WxPlugin(WeatherPlugin):
|
||||||
"""METAR Command"""
|
"""METAR Command"""
|
||||||
|
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
command_regex = "^[wx]"
|
command_regex = "^[mx]"
|
||||||
command_name = "wx (Metar)"
|
command_name = "wx (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))
|
||||||
|
api_key = self.config["aprs.fi"]["apiKey"]
|
||||||
a = re.search(r"^.*\s+(.*)", message)
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
if a is not None:
|
if a is not None:
|
||||||
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"
|
||||||
@ -125,7 +77,7 @@ class WxPlugin(plugin.APRSDPluginBase):
|
|||||||
# if no second argument, search for calling station
|
# if no second argument, search for calling station
|
||||||
fromcall = fromcall
|
fromcall = fromcall
|
||||||
try:
|
try:
|
||||||
resp = self.get_aprs(fromcall)
|
resp = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||||
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 = "Unable to find you (send beacon?)"
|
||||||
@ -135,7 +87,7 @@ class WxPlugin(plugin.APRSDPluginBase):
|
|||||||
lon = aprs_data["entries"][0]["lng"]
|
lon = aprs_data["entries"][0]["lng"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = self.get_station(lat, lon)
|
resp = self.get_weather_gov_for_gps(lat, lon)
|
||||||
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 = "Unable to find you (send beacon?)"
|
||||||
|
52
aprsd/service.py
Normal file
52
aprsd/service.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Base services class
|
||||||
|
# this is the service mechanism used to manage
|
||||||
|
# weather and location services from the config.
|
||||||
|
# There are many weather and location services
|
||||||
|
# that we could support.
|
||||||
|
import abc
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aprsd import utils, weather
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDService(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
|
config = None
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
LOG.debug("Service set config")
|
||||||
|
self.config = config
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def load(self):
|
||||||
|
"""Load and configure the service"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherService(APRSDService):
|
||||||
|
_instance = None
|
||||||
|
wx = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
# Put any init here
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
"""Load the correct weather """
|
||||||
|
wx_shortcut = self.config["aprsd"]["services"].get(
|
||||||
|
"weather",
|
||||||
|
weather.DEFAULT_PROVIDER,
|
||||||
|
)
|
||||||
|
wx_class = weather.PROVIDER_MAPPING[wx_shortcut]
|
||||||
|
self.wx = utils.create_class(wx_class, weather.APRSDWeather, config=self.config)
|
||||||
|
|
||||||
|
def forecast_short(self, lat, lon):
|
||||||
|
return self.wx.forecast_short(lat, lon)
|
||||||
|
|
||||||
|
def forecast_raw(self, lat, lon):
|
||||||
|
return self.wx.forecast_raw(lat, lon)
|
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from aprsd import plugin
|
from aprsd import plugin, weather
|
||||||
import click
|
import click
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@ -44,6 +46,9 @@ 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,
|
||||||
|
"services": {
|
||||||
|
"weather": weather.PROVIDER_MAPPING,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +57,8 @@ DEFAULT_CONFIG_DIR = "{}/.config/aprsd/".format(home)
|
|||||||
DEFAULT_SAVE_FILE = "{}/.config/aprsd/aprsd.p".format(home)
|
DEFAULT_SAVE_FILE = "{}/.config/aprsd/aprsd.p".format(home)
|
||||||
DEFAULT_CONFIG_FILE = "{}/.config/aprsd/aprsd.yml".format(home)
|
DEFAULT_CONFIG_FILE = "{}/.config/aprsd/aprsd.yml".format(home)
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
def synchronized(wrapped):
|
def synchronized(wrapped):
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
@ -222,3 +229,35 @@ def parse_config(config_file):
|
|||||||
check_option(config, "smtp", "password")
|
check_option(config, "smtp", "password")
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def create_class(module_class_string, super_cls: type = None, **kwargs):
|
||||||
|
"""
|
||||||
|
Method to create a class from a fqn python string.
|
||||||
|
:param module_class_string: full name of the class to create an object of
|
||||||
|
:param super_cls: expected super class for validity, None if bypass
|
||||||
|
:param kwargs: parameters to pass
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
module_name, class_name = module_class_string.rsplit(".", 1)
|
||||||
|
try:
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.error("Failed to load Plugin '{}' : '{}'".format(module_name, ex))
|
||||||
|
return
|
||||||
|
|
||||||
|
assert hasattr(module, class_name), "class {} is not in {}".format(
|
||||||
|
class_name,
|
||||||
|
module_name,
|
||||||
|
)
|
||||||
|
# click.echo('reading class {} from module {}'.format(
|
||||||
|
# class_name, module_name))
|
||||||
|
cls = getattr(module, class_name)
|
||||||
|
if super_cls is not None:
|
||||||
|
assert issubclass(cls, super_cls), "class {} should inherit from {}".format(
|
||||||
|
class_name,
|
||||||
|
super_cls.__name__,
|
||||||
|
)
|
||||||
|
# click.echo('initialising {} with params {}'.format(class_name, kwargs))
|
||||||
|
obj = cls(**kwargs)
|
||||||
|
return obj
|
||||||
|
66
aprsd/weather.py
Normal file
66
aprsd/weather.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import abc
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
DEFAULT_PROVIDER = "us-gov"
|
||||||
|
PROVIDER_MAPPING = {
|
||||||
|
"us-gov": "aprsd.weather.USWeatherGov",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDWeather(metaclass=abc.ABCMeta):
|
||||||
|
confg = None
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def forecast_raw(self, lat, lon):
|
||||||
|
"""Get a raw forecast json for latitude, longitude.
|
||||||
|
|
||||||
|
The format of the json response is entirely
|
||||||
|
depentent on the service itself.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def forecast_short(self, lat, lon):
|
||||||
|
"""Get a short form forecast for latitude, longitude."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class USWeatherGov(APRSDWeather):
|
||||||
|
def forecast_raw(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)
|
||||||
|
)
|
||||||
|
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 forecast_short(self, lat, lon):
|
||||||
|
"""Return a short string for the forecast."""
|
||||||
|
wx_data = self.forecast_raw(lat, lon)
|
||||||
|
reply = (
|
||||||
|
"{}F({}F/{}F) {}. {}, {}.".format(
|
||||||
|
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()
|
||||||
|
return reply
|
Loading…
Reference in New Issue
Block a user