mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-16 16:43:52 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c20705f426 |
+2
-1
@@ -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)
|
||||||
|
|||||||
+4
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
+33
-81
@@ -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?)"
|
||||||
|
|||||||
@@ -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)
|
||||||
+40
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user