mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-10-27 08:49:27 +00:00
Instantiate but disable providers. Closes #16
This commit is contained in:
@@ -7,31 +7,60 @@
|
|||||||
server-owner-callsign: "N0CALL"
|
server-owner-callsign: "N0CALL"
|
||||||
|
|
||||||
# Data providers to use. This is an example set, tailor it to your liking by commenting and uncommenting.
|
# Data providers to use. This is an example set, tailor it to your liking by commenting and uncommenting.
|
||||||
# RBN is supported but has such a high data rate, you probably don't want it enabled.
|
# RBN and APRS-IS are supported but have such a high data rate, you probably don't want them enabled.
|
||||||
# APRS-IS support is not yet implemented.
|
# Each provider needs a type, a name, and an enabled/disabled state. Some require more config such as hostnames/IP
|
||||||
|
# addresses and ports. You can duplicate them if you like, e.g. to support several DX clusters. RBN uses two ports, 7000
|
||||||
|
# for CW/RTTY and 7001 for FT8, so if you want both, you need two entries, as shown below.
|
||||||
# Feel free to write your own provider classes!
|
# Feel free to write your own provider classes!
|
||||||
providers:
|
providers:
|
||||||
# Some providers don't require any config:
|
-
|
||||||
- type: "POTA"
|
type: "POTA"
|
||||||
- type: "SOTA"
|
name: "POTA"
|
||||||
- type: "WWFF"
|
enabled: true
|
||||||
- type: "WWBOTA"
|
-
|
||||||
- type: "GMA"
|
type: "SOTA"
|
||||||
- type: "HEMA"
|
name: "SOTA"
|
||||||
- type: "ParksNPeaks"
|
enabled: true
|
||||||
# - type: "APRS-IS"
|
-
|
||||||
# Some, like DX Clusters, require extra config. You can add multiple DX clusters if you want!
|
type: "WWFF"
|
||||||
|
name: "WWFF"
|
||||||
|
enabled: true
|
||||||
|
-
|
||||||
|
type: "WWBOTA"
|
||||||
|
name: "WWBOTA"
|
||||||
|
enabled: true
|
||||||
|
-
|
||||||
|
type: "GMA"
|
||||||
|
name: "GMA"
|
||||||
|
enabled: true
|
||||||
|
-
|
||||||
|
type: "HEMA"
|
||||||
|
name: "HEMA"
|
||||||
|
enabled: true
|
||||||
|
-
|
||||||
|
type: "ParksNPeaks"
|
||||||
|
name: "ParksNPeaks"
|
||||||
|
enabled: true
|
||||||
|
-
|
||||||
|
type: "APRS-IS"
|
||||||
|
name: "APRS-IS"
|
||||||
|
enabled: false
|
||||||
-
|
-
|
||||||
type: "DXCluster"
|
type: "DXCluster"
|
||||||
|
name: "HRD Dx Cluster"
|
||||||
|
enabled: true
|
||||||
host: "hrd.wa9pie.net"
|
host: "hrd.wa9pie.net"
|
||||||
port: 8000
|
port: 8000
|
||||||
# RBN uses two ports, 7000 for CW/RTTY and 7001 for FT8, so if you want both, you need to add two entries:
|
-
|
||||||
# -
|
type: "RBN"
|
||||||
# type: "RBN"
|
name: "RBN CW/RTTY"
|
||||||
# port: 7000
|
enabled: false
|
||||||
# -
|
port: 7000
|
||||||
# type: "RBN"
|
-
|
||||||
# port: 7001
|
type: "RBN"
|
||||||
|
name: "RBN FT8"
|
||||||
|
enabled: false
|
||||||
|
port: 7001
|
||||||
|
|
||||||
# Port to open the local web server on
|
# Port to open the local web server on
|
||||||
web-server-port: 8080
|
web-server-port: 8080
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from data.band import Band
|
from data.band import Band
|
||||||
|
|
||||||
# General software
|
# General software
|
||||||
SOFTWARE_NAME = "Spothole by M0TRT"
|
SOFTWARE_NAME = "(S)pothole by M0TRT"
|
||||||
SOFTWARE_VERSION = "0.1"
|
SOFTWARE_VERSION = "0.1"
|
||||||
|
|
||||||
# Sources
|
# Sources
|
||||||
|
|||||||
30
main.py
30
main.py
@@ -35,32 +35,34 @@ def shutdown(sig, frame):
|
|||||||
logging.info("Stopping program, this may take a few seconds...")
|
logging.info("Stopping program, this may take a few seconds...")
|
||||||
global run
|
global run
|
||||||
run = False
|
run = False
|
||||||
for p in providers: p.stop()
|
for p in providers:
|
||||||
|
if p.enabled:
|
||||||
|
p.stop()
|
||||||
cleanup_timer.stop()
|
cleanup_timer.stop()
|
||||||
|
|
||||||
# Utility method to get a data provider based on its config entry.
|
# Utility method to get a data provider based on its config entry.
|
||||||
def get_provider_from_config(config_providers_entry):
|
def get_provider_from_config(config_providers_entry):
|
||||||
match config_providers_entry["type"]:
|
match config_providers_entry["type"]:
|
||||||
case "POTA":
|
case "POTA":
|
||||||
return POTA()
|
return POTA(config_providers_entry)
|
||||||
case "SOTA":
|
case "SOTA":
|
||||||
return SOTA()
|
return SOTA(config_providers_entry)
|
||||||
case "WWFF":
|
case "WWFF":
|
||||||
return WWFF()
|
return WWFF(config_providers_entry)
|
||||||
case "GMA":
|
case "GMA":
|
||||||
return GMA()
|
return GMA(config_providers_entry)
|
||||||
case "WWBOTA":
|
case "WWBOTA":
|
||||||
return WWBOTA()
|
return WWBOTA(config_providers_entry)
|
||||||
case "HEMA":
|
case "HEMA":
|
||||||
return HEMA()
|
return HEMA(config_providers_entry)
|
||||||
case "ParksNPeaks":
|
case "ParksNPeaks":
|
||||||
return ParksNPeaks()
|
return ParksNPeaks(config_providers_entry)
|
||||||
case "DXCluster":
|
case "DXCluster":
|
||||||
return DXCluster(config_providers_entry["host"], config_providers_entry["port"])
|
return DXCluster(config_providers_entry)
|
||||||
case "RBN":
|
case "RBN":
|
||||||
return RBN(config_providers_entry["port"])
|
return RBN(config_providers_entry)
|
||||||
case "APRS-IS":
|
case "APRS-IS":
|
||||||
return APRSIS()
|
return APRSIS(config_providers_entry)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -85,7 +87,9 @@ if __name__ == '__main__':
|
|||||||
# Set up data providers
|
# Set up data providers
|
||||||
for p in providers: p.setup(spot_list=spot_list)
|
for p in providers: p.setup(spot_list=spot_list)
|
||||||
# Start data providers
|
# Start data providers
|
||||||
for p in providers: p.start()
|
for p in providers:
|
||||||
|
if p.enabled:
|
||||||
|
p.start()
|
||||||
|
|
||||||
# Set up timer to clear spot list of old data
|
# Set up timer to clear spot list of old data
|
||||||
cleanup_timer = CleanupTimer(spot_list=spot_list, cleanup_interval=60, max_spot_age=config["max-spot-age-sec"])
|
cleanup_timer = CleanupTimer(spot_list=spot_list, cleanup_interval=60, max_spot_age=config["max-spot-age-sec"])
|
||||||
@@ -103,6 +107,6 @@ if __name__ == '__main__':
|
|||||||
status_data["uptime"] = str(datetime.now(pytz.UTC) - startup_time).split(".")[0]
|
status_data["uptime"] = str(datetime.now(pytz.UTC) - startup_time).split(".")[0]
|
||||||
status_data["mem_use_mb"] = round(psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024), 3)
|
status_data["mem_use_mb"] = round(psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024), 3)
|
||||||
status_data["num_spots"] = len(spot_list)
|
status_data["num_spots"] = len(spot_list)
|
||||||
status_data["providers"] = list(map(lambda p: {"name": p.name(), "status": p.status, "last_updated": p.last_update_time, "last_spot": p.last_spot_time}, providers))
|
status_data["providers"] = list(map(lambda p: {"name": p.name, "enabled": p.enabled, "status": p.status, "last_updated": p.last_update_time, "last_spot": p.last_spot_time}, providers))
|
||||||
status_data["cleanup"] = {"status": cleanup_timer.status, "last_ran": cleanup_timer.last_cleanup_time}
|
status_data["cleanup"] = {"status": cleanup_timer.status, "last_ran": cleanup_timer.last_cleanup_time}
|
||||||
status_data["webserver"] = {"status": web_server.status, "last_api_access": web_server.last_api_access_time, "last_page_access": web_server.last_page_access_time}
|
status_data["webserver"] = {"status": web_server.status, "last_api_access": web_server.last_api_access_time, "last_page_access": web_server.last_page_access_time}
|
||||||
|
|||||||
@@ -13,15 +13,12 @@ from providers.provider import Provider
|
|||||||
# Provider for the APRS-IS.
|
# Provider for the APRS-IS.
|
||||||
class APRSIS(Provider):
|
class APRSIS(Provider):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, provider_config):
|
||||||
super().__init__()
|
super().__init__(provider_config)
|
||||||
self.thread = Thread(target=self.connect)
|
self.thread = Thread(target=self.connect)
|
||||||
self.thread.daemon = True
|
self.thread.daemon = True
|
||||||
self.aprsis = None
|
self.aprsis = None
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "APRS-IS"
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
|
|
||||||
|
|||||||
@@ -21,18 +21,15 @@ class DXCluster(Provider):
|
|||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
|
|
||||||
# Constructor requires hostname and port
|
# Constructor requires hostname and port
|
||||||
def __init__(self, hostname, port):
|
def __init__(self, provider_config):
|
||||||
super().__init__()
|
super().__init__(provider_config)
|
||||||
self.hostname = hostname
|
self.hostname = provider_config["host"]
|
||||||
self.port = port
|
self.port = provider_config["port"]
|
||||||
self.telnet = None
|
self.telnet = None
|
||||||
self.thread = Thread(target=self.handle)
|
self.thread = Thread(target=self.handle)
|
||||||
self.thread.daemon = True
|
self.thread.daemon = True
|
||||||
self.run = True
|
self.run = True
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "DX Cluster " + self.hostname
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
|
|
||||||
@@ -67,7 +64,7 @@ class DXCluster(Provider):
|
|||||||
if match:
|
if match:
|
||||||
spot_time = datetime.strptime(match.group(5), "%H%MZ")
|
spot_time = datetime.strptime(match.group(5), "%H%MZ")
|
||||||
spot_datetime = datetime.combine(datetime.today(), spot_time.time()).replace(tzinfo=pytz.UTC)
|
spot_datetime = datetime.combine(datetime.today(), spot_time.time()).replace(tzinfo=pytz.UTC)
|
||||||
spot = Spot(source="Cluster",
|
spot = Spot(source=self.name,
|
||||||
dx_call=match.group(3),
|
dx_call=match.group(3),
|
||||||
de_call=match.group(1),
|
de_call=match.group(1),
|
||||||
freq=float(match.group(2)),
|
freq=float(match.group(2)),
|
||||||
|
|||||||
@@ -16,18 +16,15 @@ class GMA(HTTPProvider):
|
|||||||
REF_INFO_CACHE_TIME_DAYS = 30
|
REF_INFO_CACHE_TIME_DAYS = 30
|
||||||
REF_INFO_CACHE = CachedSession("gma_ref_info_cache", expire_after=timedelta(days=REF_INFO_CACHE_TIME_DAYS))
|
REF_INFO_CACHE = CachedSession("gma_ref_info_cache", expire_after=timedelta(days=REF_INFO_CACHE_TIME_DAYS))
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "GMA"
|
|
||||||
|
|
||||||
def http_response_to_spots(self, http_response):
|
def http_response_to_spots(self, http_response):
|
||||||
new_spots = []
|
new_spots = []
|
||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_spot in http_response.json()["RCD"]:
|
for source_spot in http_response.json()["RCD"]:
|
||||||
# Convert to our spot format
|
# Convert to our spot format
|
||||||
spot = Spot(source=self.name(),
|
spot = Spot(source=self.name,
|
||||||
dx_call=source_spot["ACTIVATOR"].upper(),
|
dx_call=source_spot["ACTIVATOR"].upper(),
|
||||||
de_call=source_spot["SPOTTER"].upper(),
|
de_call=source_spot["SPOTTER"].upper(),
|
||||||
freq=float(source_spot["QRG"]) if (source_spot["QRG"] != "") else None, # Seen GMA spots with no frequency
|
freq=float(source_spot["QRG"]) if (source_spot["QRG"] != "") else None, # Seen GMA spots with no frequency
|
||||||
|
|||||||
@@ -20,13 +20,10 @@ class HEMA(HTTPProvider):
|
|||||||
FREQ_MODE_PATTERN = re.compile("^([\\d.]*) \\((.*)\\)$")
|
FREQ_MODE_PATTERN = re.compile("^([\\d.]*) \\((.*)\\)$")
|
||||||
SPOTTER_COMMENT_PATTERN = re.compile("^\\((.*)\\) (.*)$")
|
SPOTTER_COMMENT_PATTERN = re.compile("^\\((.*)\\) (.*)$")
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SPOT_SEED_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(provider_config, self.SPOT_SEED_URL, self.POLL_INTERVAL_SEC)
|
||||||
self.spot_seed = ""
|
self.spot_seed = ""
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "HEMA"
|
|
||||||
|
|
||||||
def http_response_to_spots(self, http_response):
|
def http_response_to_spots(self, http_response):
|
||||||
# OK, source data is actually just the spot seed at this point. We'll then go on to fetch real data if we know
|
# OK, source data is actually just the spot seed at this point. We'll then go on to fetch real data if we know
|
||||||
# this has changed.
|
# this has changed.
|
||||||
@@ -48,7 +45,7 @@ class HEMA(HTTPProvider):
|
|||||||
spotter_comment_match = re.search(self.SPOTTER_COMMENT_PATTERN, spot_items[6])
|
spotter_comment_match = re.search(self.SPOTTER_COMMENT_PATTERN, spot_items[6])
|
||||||
|
|
||||||
# Convert to our spot format
|
# Convert to our spot format
|
||||||
spot = Spot(source=self.name(),
|
spot = Spot(source=self.name,
|
||||||
dx_call=spot_items[2].upper(),
|
dx_call=spot_items[2].upper(),
|
||||||
de_call=spotter_comment_match.group(1).upper(),
|
de_call=spotter_comment_match.group(1).upper(),
|
||||||
freq=float(freq_mode_match.group(1)) * 1000,
|
freq=float(freq_mode_match.group(1)) * 1000,
|
||||||
|
|||||||
@@ -13,20 +13,17 @@ from providers.provider import Provider
|
|||||||
# duplication. Subclasses of this query the individual APIs for data.
|
# duplication. Subclasses of this query the individual APIs for data.
|
||||||
class HTTPProvider(Provider):
|
class HTTPProvider(Provider):
|
||||||
|
|
||||||
def __init__(self, url, poll_interval):
|
def __init__(self, provider_config, url, poll_interval):
|
||||||
super().__init__()
|
super().__init__(provider_config)
|
||||||
self.url = url
|
self.url = url
|
||||||
self.poll_interval = poll_interval
|
self.poll_interval = poll_interval
|
||||||
self.poll_timer = None
|
self.poll_timer = None
|
||||||
|
|
||||||
def name(self):
|
|
||||||
raise NotImplementedError("Subclasses must implement this method")
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
# Fire off a one-shot thread to run poll() for the first time, just to ensure start() returns immediately and
|
# Fire off a one-shot thread to run poll() for the first time, just to ensure start() returns immediately and
|
||||||
# the application can continue starting. The thread itself will then die, and the timer will kick in on its own
|
# the application can continue starting. The thread itself will then die, and the timer will kick in on its own
|
||||||
# thread.
|
# thread.
|
||||||
logging.info("Set up query of " + self.name() + " API every " + str(self.poll_interval) + " seconds.")
|
logging.info("Set up query of " + self.name + " API every " + str(self.poll_interval) + " seconds.")
|
||||||
thread = Thread(target=self.poll)
|
thread = Thread(target=self.poll)
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
@@ -37,7 +34,7 @@ class HTTPProvider(Provider):
|
|||||||
def poll(self):
|
def poll(self):
|
||||||
try:
|
try:
|
||||||
# Request data from API
|
# Request data from API
|
||||||
logging.debug("Polling " + self.name() + " API...")
|
logging.debug("Polling " + self.name + " API...")
|
||||||
http_response = requests.get(self.url, headers=self.HTTP_HEADERS)
|
http_response = requests.get(self.url, headers=self.HTTP_HEADERS)
|
||||||
# Pass off to the subclass for processing
|
# Pass off to the subclass for processing
|
||||||
new_spots = self.http_response_to_spots(http_response)
|
new_spots = self.http_response_to_spots(http_response)
|
||||||
@@ -47,11 +44,11 @@ class HTTPProvider(Provider):
|
|||||||
|
|
||||||
self.status = "OK"
|
self.status = "OK"
|
||||||
self.last_update_time = datetime.now(pytz.UTC)
|
self.last_update_time = datetime.now(pytz.UTC)
|
||||||
logging.debug("Received data from " + self.name() + " API.")
|
logging.debug("Received data from " + self.name + " API.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logging.exception("Exception in HTTP JSON Provider (" + self.name() + ")")
|
logging.exception("Exception in HTTP JSON Provider (" + self.name + ")")
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
self.poll_timer = Timer(self.poll_interval, self.poll)
|
self.poll_timer = Timer(self.poll_interval, self.poll)
|
||||||
|
|||||||
@@ -12,18 +12,15 @@ class ParksNPeaks(HTTPProvider):
|
|||||||
POLL_INTERVAL_SEC = 120
|
POLL_INTERVAL_SEC = 120
|
||||||
SPOTS_URL = "https://www.parksnpeaks.org/api/ALL"
|
SPOTS_URL = "https://www.parksnpeaks.org/api/ALL"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "ParksNPeaks"
|
|
||||||
|
|
||||||
def http_response_to_spots(self, http_response):
|
def http_response_to_spots(self, http_response):
|
||||||
new_spots = []
|
new_spots = []
|
||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_spot in http_response.json():
|
for source_spot in http_response.json():
|
||||||
# Convert to our spot format
|
# Convert to our spot format
|
||||||
spot = Spot(source=self.name(),
|
spot = Spot(source=self.name,
|
||||||
source_id=source_spot["actID"],
|
source_id=source_spot["actID"],
|
||||||
dx_call=source_spot["actCallsign"].upper(),
|
dx_call=source_spot["actCallsign"].upper(),
|
||||||
de_call=source_spot["actSpoter"].upper(), # typo exists in API
|
de_call=source_spot["actSpoter"].upper(), # typo exists in API
|
||||||
|
|||||||
@@ -11,18 +11,15 @@ class POTA(HTTPProvider):
|
|||||||
POLL_INTERVAL_SEC = 120
|
POLL_INTERVAL_SEC = 120
|
||||||
SPOTS_URL = "https://api.pota.app/spot/activator"
|
SPOTS_URL = "https://api.pota.app/spot/activator"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "POTA"
|
|
||||||
|
|
||||||
def http_response_to_spots(self, http_response):
|
def http_response_to_spots(self, http_response):
|
||||||
new_spots = []
|
new_spots = []
|
||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_spot in http_response.json():
|
for source_spot in http_response.json():
|
||||||
# Convert to our spot format
|
# Convert to our spot format
|
||||||
spot = Spot(source=self.name(),
|
spot = Spot(source=self.name,
|
||||||
source_id=source_spot["spotId"],
|
source_id=source_spot["spotId"],
|
||||||
dx_call=source_spot["activator"].upper(),
|
dx_call=source_spot["activator"].upper(),
|
||||||
de_call=source_spot["spotter"].upper(),
|
de_call=source_spot["spotter"].upper(),
|
||||||
|
|||||||
@@ -13,16 +13,14 @@ class Provider:
|
|||||||
HTTP_HEADERS = { "User-Agent": SOFTWARE_NAME + " " + SOFTWARE_VERSION + " (operated by " + config["server-owner-callsign"] + ")" }
|
HTTP_HEADERS = { "User-Agent": SOFTWARE_NAME + " " + SOFTWARE_VERSION + " (operated by " + config["server-owner-callsign"] + ")" }
|
||||||
|
|
||||||
# Constructor
|
# Constructor
|
||||||
def __init__(self):
|
def __init__(self, provider_config):
|
||||||
|
self.name = provider_config["name"]
|
||||||
|
self.enabled = provider_config["enabled"]
|
||||||
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||||
self.last_spot_time = datetime.min.replace(tzinfo=pytz.UTC)
|
self.last_spot_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||||
self.status = "Not Started"
|
self.status = "Not Started" if self.enabled else "Disabled"
|
||||||
self.spot_list = None
|
self.spot_list = None
|
||||||
|
|
||||||
# Return the name of the provider
|
|
||||||
def name(self):
|
|
||||||
raise NotImplementedError("Subclasses must implement this method")
|
|
||||||
|
|
||||||
# Set up the provider, e.g. giving it the spot list to work from
|
# Set up the provider, e.g. giving it the spot list to work from
|
||||||
def setup(self, spot_list):
|
def setup(self, spot_list):
|
||||||
self.spot_list = spot_list
|
self.spot_list = spot_list
|
||||||
|
|||||||
@@ -22,17 +22,14 @@ class RBN(Provider):
|
|||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
|
|
||||||
# Constructor requires port number.
|
# Constructor requires port number.
|
||||||
def __init__(self, port):
|
def __init__(self, provider_config):
|
||||||
super().__init__()
|
super().__init__(provider_config)
|
||||||
self.port = port
|
self.port = provider_config["port"]
|
||||||
self.telnet = None
|
self.telnet = None
|
||||||
self.thread = Thread(target=self.handle)
|
self.thread = Thread(target=self.handle)
|
||||||
self.thread.daemon = True
|
self.thread.daemon = True
|
||||||
self.run = True
|
self.run = True
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "RBN port " + str(self.port)
|
|
||||||
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
@@ -68,7 +65,7 @@ class RBN(Provider):
|
|||||||
if match:
|
if match:
|
||||||
spot_time = datetime.strptime(match.group(5), "%H%MZ")
|
spot_time = datetime.strptime(match.group(5), "%H%MZ")
|
||||||
spot_datetime = datetime.combine(datetime.today(), spot_time.time()).replace(tzinfo=pytz.UTC)
|
spot_datetime = datetime.combine(datetime.today(), spot_time.time()).replace(tzinfo=pytz.UTC)
|
||||||
spot = Spot(source="RBN",
|
spot = Spot(source=self.name,
|
||||||
dx_call=match.group(3),
|
dx_call=match.group(3),
|
||||||
de_call=match.group(1),
|
de_call=match.group(1),
|
||||||
freq=float(match.group(2)),
|
freq=float(match.group(2)),
|
||||||
|
|||||||
@@ -20,13 +20,10 @@ class SOTA(HTTPProvider):
|
|||||||
SUMMIT_DATA_CACHE_TIME_DAYS = 30
|
SUMMIT_DATA_CACHE_TIME_DAYS = 30
|
||||||
SUMMIT_DATA_CACHE = CachedSession("sota_summit_data_cache", expire_after=timedelta(days=SUMMIT_DATA_CACHE_TIME_DAYS))
|
SUMMIT_DATA_CACHE = CachedSession("sota_summit_data_cache", expire_after=timedelta(days=SUMMIT_DATA_CACHE_TIME_DAYS))
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.EPOCH_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(provider_config, self.EPOCH_URL, self.POLL_INTERVAL_SEC)
|
||||||
self.api_epoch = ""
|
self.api_epoch = ""
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "SOTA"
|
|
||||||
|
|
||||||
def http_response_to_spots(self, http_response):
|
def http_response_to_spots(self, http_response):
|
||||||
# OK, source data is actually just the epoch at this point. We'll then go on to fetch real data if we know this
|
# OK, source data is actually just the epoch at this point. We'll then go on to fetch real data if we know this
|
||||||
# has changed.
|
# has changed.
|
||||||
@@ -40,7 +37,7 @@ class SOTA(HTTPProvider):
|
|||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_spot in source_data:
|
for source_spot in source_data:
|
||||||
# Convert to our spot format
|
# Convert to our spot format
|
||||||
spot = Spot(source=self.name(),
|
spot = Spot(source=self.name,
|
||||||
source_id=source_spot["id"],
|
source_id=source_spot["id"],
|
||||||
dx_call=source_spot["activatorCallsign"].upper(),
|
dx_call=source_spot["activatorCallsign"].upper(),
|
||||||
dx_name=source_spot["activatorName"],
|
dx_name=source_spot["activatorName"],
|
||||||
|
|||||||
@@ -9,11 +9,8 @@ class WWBOTA(HTTPProvider):
|
|||||||
POLL_INTERVAL_SEC = 120
|
POLL_INTERVAL_SEC = 120
|
||||||
SPOTS_URL = "https://api.wwbota.org/spots/"
|
SPOTS_URL = "https://api.wwbota.org/spots/"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "WWBOTA"
|
|
||||||
|
|
||||||
def http_response_to_spots(self, http_response):
|
def http_response_to_spots(self, http_response):
|
||||||
new_spots = []
|
new_spots = []
|
||||||
@@ -26,7 +23,7 @@ class WWBOTA(HTTPProvider):
|
|||||||
for ref in source_spot["references"]:
|
for ref in source_spot["references"]:
|
||||||
refs.append(ref["reference"])
|
refs.append(ref["reference"])
|
||||||
ref_names.append(ref["name"])
|
ref_names.append(ref["name"])
|
||||||
spot = Spot(source=self.name(),
|
spot = Spot(source=self.name,
|
||||||
dx_call=source_spot["call"].upper(),
|
dx_call=source_spot["call"].upper(),
|
||||||
de_call=source_spot["spotter"].upper(),
|
de_call=source_spot["spotter"].upper(),
|
||||||
freq=float(source_spot["freq"]) * 1000, # MHz to kHz
|
freq=float(source_spot["freq"]) * 1000, # MHz to kHz
|
||||||
|
|||||||
@@ -11,18 +11,15 @@ class WWFF(HTTPProvider):
|
|||||||
POLL_INTERVAL_SEC = 120
|
POLL_INTERVAL_SEC = 120
|
||||||
SPOTS_URL = "https://spots.wwff.co/static/spots.json"
|
SPOTS_URL = "https://spots.wwff.co/static/spots.json"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
|
||||||
|
|
||||||
def name(self):
|
|
||||||
return "WWFF"
|
|
||||||
|
|
||||||
def http_response_to_spots(self, http_response):
|
def http_response_to_spots(self, http_response):
|
||||||
new_spots = []
|
new_spots = []
|
||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_spot in http_response.json():
|
for source_spot in http_response.json():
|
||||||
# Convert to our spot format
|
# Convert to our spot format
|
||||||
spot = Spot(source=self.name(),
|
spot = Spot(source=self.name,
|
||||||
source_id=source_spot["id"],
|
source_id=source_spot["id"],
|
||||||
dx_call=source_spot["activator"].upper(),
|
dx_call=source_spot["activator"].upper(),
|
||||||
de_call=source_spot["spotter"].upper(),
|
de_call=source_spot["spotter"].upper(),
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ class WebServer:
|
|||||||
bottle.get("/api/options")(self.serve_api_options)
|
bottle.get("/api/options")(self.serve_api_options)
|
||||||
bottle.get("/api/status")(self.serve_api_status)
|
bottle.get("/api/status")(self.serve_api_status)
|
||||||
bottle.get("/")(self.serve_index)
|
bottle.get("/")(self.serve_index)
|
||||||
bottle.get("/apidocs")(self.serve_apidocs)
|
|
||||||
bottle.get("/<filepath:path>")(self.serve_static_file)
|
bottle.get("/<filepath:path>")(self.serve_static_file)
|
||||||
|
|
||||||
# Start the web server
|
# Start the web server
|
||||||
@@ -69,19 +68,20 @@ class WebServer:
|
|||||||
|
|
||||||
# Serve the home page. This would be accessible as /index.html but we need this workaround to make it available as /
|
# Serve the home page. This would be accessible as /index.html but we need this workaround to make it available as /
|
||||||
def serve_index(self):
|
def serve_index(self):
|
||||||
self.last_page_access_time = datetime.now(pytz.UTC)
|
return self.serve_static_file("")
|
||||||
self.status = "OK"
|
|
||||||
return bottle.static_file("index.html", root="webassets")
|
|
||||||
|
|
||||||
# Serve the API docs page. This would be accessible as /apidocs/index.html but we need this workaround to make it
|
# Serve general static files from "webassets" directory, along with some extra workarounds to make URLs such as
|
||||||
# available as /apidocs
|
# "/", "/about" and "/apidocs" work.
|
||||||
def serve_apidocs(self):
|
|
||||||
self.last_page_access_time = datetime.now(pytz.UTC)
|
|
||||||
self.status = "OK"
|
|
||||||
return bottle.static_file("index.html", root="webassets/apidocs")
|
|
||||||
|
|
||||||
# Serve general static files from "webassets" directory
|
|
||||||
def serve_static_file(self, filepath):
|
def serve_static_file(self, filepath):
|
||||||
|
self.last_page_access_time = datetime.now(pytz.UTC)
|
||||||
|
self.status = "OK"
|
||||||
|
if filepath == "":
|
||||||
|
return bottle.static_file("index.html", root="webassets")
|
||||||
|
elif filepath == "about":
|
||||||
|
return bottle.static_file("about.html", root="webassets")
|
||||||
|
elif filepath == "apidocs":
|
||||||
|
return bottle.static_file("index.html", root="webassets/apidocs")
|
||||||
|
else:
|
||||||
return bottle.static_file(filepath, root="webassets")
|
return bottle.static_file(filepath, root="webassets")
|
||||||
|
|
||||||
# Utility method to apply filters to the overall spot list and return only a subset. Enables query parameters in
|
# Utility method to apply filters to the overall spot list and return only a subset. Enables query parameters in
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
openapi: 3.0.4
|
openapi: 3.0.4
|
||||||
info:
|
info:
|
||||||
title: Spothole API
|
title: (S)pothole API
|
||||||
description: |-
|
description: |-
|
||||||
Spothole is a utility to aggregate "spots" from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data.
|
(S)pothole is a utility to aggregate "spots" from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data.
|
||||||
|
|
||||||
While there are other web-based interfaces to DX clusters, and sites that aggregate spots from various outfoor activity programmes for amateur radio, Spothole differentiates itself by supporting a large number of data sources, and by being "API first" rather than just providing a web front-end. This allows other software to be built on top of it. Spothole itself is also open source, Public Domain licenced code that anyone can take and modify.
|
While there are other web-based interfaces to DX clusters, and sites that aggregate spots from various outfoor activity programmes for amateur radio, (S)pothole differentiates itself by supporting a large number of data sources, and by being "API first" rather than just providing a web front-end. This allows other software to be built on top of it. Spothole itself is also open source, Public Domain licenced code that anyone can take and modify.
|
||||||
contact:
|
contact:
|
||||||
email: ian@ianrenton.com
|
email: ian@ianrenton.com
|
||||||
license:
|
license:
|
||||||
@@ -531,6 +531,10 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: The name of the provider.
|
description: The name of the provider.
|
||||||
example: POTA
|
example: POTA
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the provider is enabled or not.
|
||||||
|
example: true
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
description: The status of the provider.
|
description: The status of the provider.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<title>Spothole</title>
|
<title>(S)pothole</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="css/style.css" type="text/css">
|
<link rel="stylesheet" href="css/style.css" type="text/css">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom">
|
<header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom">
|
||||||
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||||
<span class="fs-4">Spothole</span>
|
<span class="fs-4">(S)pothole</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
@@ -36,11 +36,11 @@
|
|||||||
<main>
|
<main>
|
||||||
|
|
||||||
<div id="info-container">
|
<div id="info-container">
|
||||||
<h3>About Spothole</h3>
|
<h3>About (S)pothole</h3>
|
||||||
<p>Spothole is a utility to aggregate "spots" from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data.</p>
|
<p>(S)pothole is a utility to aggregate "spots" from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data.</p>
|
||||||
<p>While there are several other web-based interfaces to DX clusters, and sites that aggregate spots from various outfoor activity programmes for amateur radio, Spothole differentiates itself by supporting a large number of data sources, and by being "API first" rather than just providing a web front-end. This allows other software to be built on top of it.</p>
|
<p>While there are several other web-based interfaces to DX clusters, and sites that aggregate spots from various outfoor activity programmes for amateur radio, (S)pothole differentiates itself by supporting a large number of data sources, and by being "API first" rather than just providing a web front-end. This allows other software to be built on top of it.</p>
|
||||||
<p>The API is deliberately well-defined with an <a href="/apidocs/openapi.yml">OpenAPI specification</a> and auto-generated <a href="/apidocs">API documentation</a>. The API delivers spots in a consistent format regardless of the data source, freeing developers from needing to know how each individual data source presents its data.</p>
|
<p>The API is deliberately well-defined with an <a href="/apidocs/openapi.yml">OpenAPI specification</a> and auto-generated <a href="/apidocs">API documentation</a>. The API delivers spots in a consistent format regardless of the data source, freeing developers from needing to know how each individual data source presents its data.</p>
|
||||||
<p>Spothole itself is also open source, Public Domain licenced code that anyone can take and modify. <a href="https://git.ianrenton.com/ian/metaspot/">The source code is here</a>.</p>
|
<p>(S)pothole itself is also open source, Public Domain licenced code that anyone can take and modify. <a href="https://git.ianrenton.com/ian/metaspot/">The source code is here</a>.</p>
|
||||||
<p>Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, and Parks 'n' Peaks.</p>
|
<p>Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, and Parks 'n' Peaks.</p>
|
||||||
<p>The software was written by <a href="https://ianrenton.com">Ian Renton, MØTRT</a>.</p>
|
<p>The software was written by <a href="https://ianrenton.com">Ian Renton, MØTRT</a>.</p>
|
||||||
<p><a href="#" onclick="hideInfo()">« Back to the spots table</a></p>
|
<p><a href="#" onclick="hideInfo()">« Back to the spots table</a></p>
|
||||||
|
|||||||
Reference in New Issue
Block a user