diff --git a/README.md b/README.md index 8d7eb9a..6a9a325 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The API is deliberately well-defined with an OpenAPI specification and auto-gene Spothole itself is also open source, Public Domain licenced code that anyone can take and modify. -Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, BOTA, the UK Packet Repeater Network, NG3K, and any site based on the xOTA software by nischu. +Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, BOTA, LLOTA, the UK Packet Repeater Network, NG3K, and any site based on the xOTA software by nischu.  diff --git a/config-example.yml b/config-example.yml index 5d65d6f..f2dc1b4 100644 --- a/config-example.yml +++ b/config-example.yml @@ -49,6 +49,10 @@ spot-providers: class: "WOTA" name: "WOTA" enabled: true + - + class: "LLOTA" + name: "LLOTA" + enabled: true - class: "APRSIS" name: "APRS-IS" diff --git a/core/constants.py b/core/constants.py index 5ae974f..631d719 100644 --- a/core/constants.py +++ b/core/constants.py @@ -28,6 +28,8 @@ SIGS = [ SIG(name="WOTA", description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}"), SIG(name="BOTA", description="Beaches on the Air"), SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award"), + SIG(name="LLOTA", description="Lagos y Lagunas on the Air", ref_regex=r"[A-Z]{2}\-\d{4}"), + SIG(name="WWTOTA", description="Towers on the Air", ref_regex=r"[A-Z]{2}R\-\d{4}"), SIG(name="WAB", description="Worked All Britain", ref_regex=r"[A-Z]{1,2}[0-9]{2}"), SIG(name="WAI", description="Worked All Ireland", ref_regex=r"[A-Z][0-9]{2}"), SIG(name="TOTA", description="Toilets on the Air", ref_regex=r"T\-[0-9]{2}") diff --git a/core/sig_utils.py b/core/sig_utils.py index 40b8432..f3f1260 100644 --- a/core/sig_utils.py +++ b/core/sig_utils.py @@ -1,7 +1,7 @@ import csv import logging -from pyhamtools.locator import latlong_to_locator +from pyhamtools.locator import latlong_to_locator, locator_to_latlong from core.cache_utils import SEMI_STATIC_URL_DATA_CACHE from core.constants import SIGS, HTTP_HEADERS @@ -21,7 +21,7 @@ def get_ref_regex_for_sig(sig): # Note there is currently no support for KRMNPA location lookup, see issue #61. def populate_sig_ref_info(sig_ref): if sig_ref.sig is None or sig_ref.id is None: - logging.warn("Failed to look up sig_ref info, sig or id were not set.") + logging.warning("Failed to look up sig_ref info, sig or id were not set.") sig = sig_ref.sig ref_id = sig_ref.id @@ -123,6 +123,18 @@ def populate_sig_ref_info(sig_ref): if not sig_ref.name: sig_ref.name = sig_ref.id sig_ref.url = "https://www.beachesontheair.com/beaches/" + sig_ref.name.lower().replace(" ", "-") + elif sig.upper() == "LLOTA": + data = SEMI_STATIC_URL_DATA_CACHE.get("https://llota.app/api/public/references", headers=HTTP_HEADERS).json() + if data: + for ref in data: + if ref["reference_code"] == ref_id: + sig_ref.name = ref["name"] + sig_ref.url = "https://llota.app/list/ref/" + ref_id + sig_ref.grid = ref["grid_locator"] + ll = locator_to_latlong(sig_ref.grid) + sig_ref.latitude = ll[0] + sig_ref.longitude = ll[1] + break elif sig.upper() == "WAB" or sig.upper() == "WAI": ll = wab_wai_square_to_lat_lon(ref_id) if ll: @@ -134,7 +146,7 @@ def populate_sig_ref_info(sig_ref): except: logging.debug("Invalid lat/lon received for reference") except: - logging.warn("Failed to look up sig_ref info for " + sig + " ref " + ref_id + ".") + logging.warning("Failed to look up sig_ref info for " + sig + " ref " + ref_id + ".") return sig_ref diff --git a/spotproviders/llota.py b/spotproviders/llota.py new file mode 100644 index 0000000..2c362f9 --- /dev/null +++ b/spotproviders/llota.py @@ -0,0 +1,41 @@ +from datetime import datetime + +from data.sig_ref import SIGRef +from data.spot import Spot +from spotproviders.http_spot_provider import HTTPSpotProvider + + +# Spot provider for Lagos y Lagunas On the Air +class LLOTA(HTTPSpotProvider): + POLL_INTERVAL_SEC = 120 + SPOTS_URL = "https://llota.app/api/public/spots" + + def __init__(self, provider_config): + super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC) + + def http_response_to_spots(self, http_response): + new_spots = [] + # Iterate through source data + for source_spot in http_response.json(): + # Find the most recent spotter and comment from the history array + comment = None + spotter = None + if "history" in source_spot and len(source_spot["history"]) > 0: + comment = source_spot["history"][-1]["comment"] + spotter = source_spot["history"][-1]["spotter_callsign"] + # Convert to our spot format + spot = Spot(source=self.name, + source_id=source_spot["id"], + dx_call=source_spot["callsign"].upper(), + de_call=spotter.upper() if spotter else None, + freq=float(source_spot["frequency"]) * 1000000, + mode=source_spot["mode"].upper(), + comment=comment, + sig="LLOTA", + sig_refs=[SIGRef(id=source_spot["reference"], sig="LLOTA", name=source_spot["reference_name"])], + time=datetime.fromisoformat(source_spot["updated_at"].replace("Z", "+00:00")).timestamp()) + + # Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do + # that for us. + new_spots.append(spot) + return new_spots \ No newline at end of file diff --git a/spotproviders/pota.py b/spotproviders/pota.py index 13f75d5..08f8ab1 100644 --- a/spotproviders/pota.py +++ b/spotproviders/pota.py @@ -11,8 +11,6 @@ from spotproviders.http_spot_provider import HTTPSpotProvider class POTA(HTTPSpotProvider): POLL_INTERVAL_SEC = 120 SPOTS_URL = "https://api.pota.app/spot/activator" - # Might need to look up extra park data - PARK_URL_ROOT = "https://api.pota.app/park/" def __init__(self, provider_config): super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC) diff --git a/templates/about.html b/templates/about.html index ecade7a..b28c490 100644 --- a/templates/about.html +++ b/templates/about.html @@ -25,10 +25,10 @@
In amateur radio terminology, the "DX" contact is the "interesting" one that is using the frequency shown and looking for callers. They might be on a remote island or just in a local park, but either way it's interesting enough that someone has "spotted" them. The callsign listed under "DE" is the person who entered the spot of the "DX" operator. "Modes" are the type of communication they are using. For example you might see "CW" which is Morse Code, or voice "modes" like SSB or FM, or more exotic "data" modes which are used for computer-to-computer communication.
Spothole can retrieve spots from: Telnet-based DX clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, the UK Packet Repeater Network, and any site based on the xOTA software by nischu.
+Spothole can retrieve spots from: Telnet-based DX clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, Parks 'n' Peaks, ZLOTA, WOTA, LLOTA, the UK Packet Repeater Network, and any site based on the xOTA software by nischu.
Spothole can retrieve alerts from: NG3K, POTA, SOTA, WWFF, Parks 'n' Peaks, WOTA and BOTA.
Note that the server owner has not necessarily enabled all these data sources. In particular it is common to disable RBN, to avoid the server being swamped with FT8 traffic, and to disable APRS-IS and UK Packet Net so that the server only displays stations where there is likely to be an operator physically present for a QSO.
-Between the various data sources, the following Special Interest Groups (SIGs) are supported: Parks on the Air (POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air (MOTA), the Amateur Radio Lighthouse Socirty (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National Parks Award (KRMNPA), Wainwrights on the Air (WOTA), Beaches on the Air (BOTA), Worked All Britain (WAB), Worked All Ireland (WAI), and Toilets on the Air (TOTA).
+Between the various data sources, the following Special Interest Groups (SIGs) are supported: Parks on the Air (POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air (MOTA), the Amateur Radio Lighthouse Socirty (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National Parks Award (KRMNPA), Wainwrights on the Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air (WWTOTA), Worked All Britain (WAB), Worked All Ireland (WAI), and Toilets on the Air (TOTA).
As of the time of writing in November 2025, I think Spothole captures essentially all outdoor radio programmes that have a defined reference list, and almost certainly those that have a spotting/alerting API. If you know of one I've missed, please let me know!
Mostly, but not quite. While POTA spots generally come from the POTA source and so on, there are a few exceptions:
diff --git a/webassets/apidocs/openapi.yml b/webassets/apidocs/openapi.yml index 5e3806a..08994c6 100644 --- a/webassets/apidocs/openapi.yml +++ b/webassets/apidocs/openapi.yml @@ -752,6 +752,8 @@ components: - ParksNPeaks - ZLOTA - WOTA + - LLOTA + - WWTOTA - Cluster - RBN - APRS-IS @@ -777,6 +779,8 @@ components: - IOTA - WOTA - BOTA + - LLOTA + - WWTOTA - WAB - WAI - TOTA @@ -801,6 +805,8 @@ components: - IOTA - WOTA - BOTA + - LLOTA + - WWTOTA - WAB - WAI - TOTA