From 5d4b3d500df0fbc6761d6097b2f255163cd0f11d Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Thu, 23 Oct 2025 08:47:00 +0100 Subject: [PATCH] Get ZLOTA spots from its own API rather than PnP. Closes #37 --- alertproviders/parksnpeaks.py | 3 +- alertproviders/wota.py | 3 +- core/constants.py | 3 +- spotproviders/parksnpeaks.py | 21 +++---------- spotproviders/zlota.py | 59 +++++++++++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 spotproviders/zlota.py diff --git a/alertproviders/parksnpeaks.py b/alertproviders/parksnpeaks.py index 353dea3..e90a02a 100644 --- a/alertproviders/parksnpeaks.py +++ b/alertproviders/parksnpeaks.py @@ -49,7 +49,8 @@ class ParksNPeaks(HTTPAlertProvider): logging.warn("PNP alert found with sig " + alert.sig + ", developer needs to add support for this!") # If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to - # the alert list. + # the alert list. Note that while ZLOTA has its own spots API, it doesn't have its own alerts API. So that + # means the PnP *spot* provider rejects ZLOTA spots here, but the PnP *alerts* provider here allows ZLOTA. if alert.sig not in ["POTA", "SOTA", "WWFF"]: new_alerts.append(alert) return new_alerts diff --git a/alertproviders/wota.py b/alertproviders/wota.py index 31dcf95..3be5006 100644 --- a/alertproviders/wota.py +++ b/alertproviders/wota.py @@ -5,6 +5,7 @@ import pytz from rss_parser import RSSParser from alertproviders.http_alert_provider import HTTPAlertProvider +from core.sig_utils import get_icon_for_sig from data.alert import Alert @@ -51,7 +52,7 @@ class WOTA(HTTPAlertProvider): sig="WOTA", sig_refs=[ref] if ref else [], sig_refs_names=[ref_name] if ref_name else [], - icon="mound", + icon=get_icon_for_sig("WOTA"), start_time=time.timestamp()) # Add to our list. diff --git a/core/constants.py b/core/constants.py index 96c0c27..c7294a5 100644 --- a/core/constants.py +++ b/core/constants.py @@ -26,7 +26,8 @@ SIGS = [ SIG(name="ZLOTA", description="New Zealand on the Air", icon="kiwi-bird", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d+"), SIG(name="KRMNPA", description="Keith Roget Memorial National Parks Award", icon="earth-oceania", ref_regex=r""), SIG(name="WAB", description="Worked All Britain", icon="table-cells-large", ref_regex=r"[A-Z]{1,2}[0-9]{2}"), - SIG(name="WAI", description="Worked All Ireland", icon="table-cells-large", ref_regex=r"[A-Z][0-9]{2}") + SIG(name="WAI", description="Worked All Ireland", icon="table-cells-large", ref_regex=r"[A-Z][0-9]{2}"), + SIG(name="WOTA", description="Wainwrights on the Air", icon="w", ref_regex=r"[A-Z]{3}-[0-9]{2}") ] # Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA". diff --git a/spotproviders/parksnpeaks.py b/spotproviders/parksnpeaks.py index bb992f0..3f54fd6 100644 --- a/spotproviders/parksnpeaks.py +++ b/spotproviders/parksnpeaks.py @@ -19,9 +19,6 @@ class ParksNPeaks(HTTPSpotProvider): SIOTA_LIST_URL = "https://www.silosontheair.com/data/silos.csv" SIOTA_LIST_CACHE_TIME_DAYS = 30 SIOTA_LIST_CACHE = CachedSession("cache/siota_data_cache", expire_after=timedelta(days=SIOTA_LIST_CACHE_TIME_DAYS)) - ZLOTA_LIST_URL = "https://ontheair.nz/assets/assets.json" - ZLOTA_LIST_CACHE_TIME_DAYS = 30 - ZLOTA_LIST_CACHE = CachedSession("cache/zlota_data_cache", expire_after=timedelta(days=ZLOTA_LIST_CACHE_TIME_DAYS)) def __init__(self, provider_config): super().__init__(provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC) @@ -52,7 +49,7 @@ class ParksNPeaks(HTTPSpotProvider): # Extract a de_call if it's in the comment but not in the "actSpoter" field m = re.search(r"\(de ([A-Za-z0-9]*)\)", spot.comment) - if (not spot.de_call or spot.de_call == "ZLOTA") and m: + if not spot.de_call and m: spot.de_call = m.group(1) # Log a warning for the developer if PnP gives us an unknown programme we've never seen before @@ -70,20 +67,10 @@ class ParksNPeaks(HTTPSpotProvider): spot.dx_grid = row["LOCATOR"] break - # ZLOTA name/lat/lon lookup - if spot.sig == "ZLOTA": - zlota_data = self.ZLOTA_LIST_CACHE.get(self.ZLOTA_LIST_URL, headers=HTTP_HEADERS).json() - for asset in zlota_data: - if asset["code"] == spot.sig_refs[0]: - spot.sig_refs_names = [asset["name"]] - spot.dx_latitude = asset["y"] - spot.dx_longitude = asset["x"] - break - # Note there is currently no support for KRMNPA location lookup, see issue #61. - # If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to - # the spot list. - if spot.sig not in ["POTA", "SOTA", "WWFF"]: + # If this is POTA, SOTA, WWFF or ZLOTA data we already have it through other means, so ignore. Otherwise, + # add to the spot list. + if spot.sig not in ["POTA", "SOTA", "WWFF", "ZLOTA"]: new_spots.append(spot) return new_spots diff --git a/spotproviders/zlota.py b/spotproviders/zlota.py new file mode 100644 index 0000000..7cd7380 --- /dev/null +++ b/spotproviders/zlota.py @@ -0,0 +1,59 @@ +import csv +import logging +import re +from datetime import datetime, timedelta + +import pytz +from requests_cache import CachedSession + +from core.constants import HTTP_HEADERS +from core.sig_utils import get_icon_for_sig +from data.spot import Spot +from spotproviders.http_spot_provider import HTTPSpotProvider + + +# Spot provider for ZLOTA +class ZLOTA(HTTPSpotProvider): + POLL_INTERVAL_SEC = 120 + SPOTS_URL = "https://ontheair.nz/api/spots?zlota_only=true" + LIST_URL = "https://ontheair.nz/assets/assets.json" + LIST_CACHE_TIME_DAYS = 30 + LIST_CACHE = CachedSession("cache/zlota_data_cache", expire_after=timedelta(days=LIST_CACHE_TIME_DAYS)) + + 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(): + # Frequency is often inconsistent as to whether it's in Hz or kHz. Make a guess. + freq_hz = float(source_spot["frequency"]) + if freq_hz < 1000000: + freq_hz = freq_hz * 1000 + + # Convert to our spot format + spot = Spot(source=self.name, + source_id=source_spot["id"], + dx_call=source_spot["activator"].upper(), + de_call=source_spot["spotter"].upper(), + freq=freq_hz, + mode=source_spot["mode"].upper().strip(), + comment=source_spot["comments"], + sig="ZLOTA", + sig_refs=[source_spot["reference"]], + sig_refs_names=[source_spot["name"]], + icon=get_icon_for_sig("ZLOTA"), + time=datetime.fromisoformat(source_spot["referenced_time"]).astimezone(pytz.UTC).timestamp()) + + # ZLOTA name/lat/lon lookup + zlota_data = self.LIST_CACHE.get(self.LIST_URL, headers=HTTP_HEADERS).json() + for asset in zlota_data: + if asset["code"] == spot.sig_refs[0]: + spot.sig_refs_names = [asset["name"]] + spot.dx_latitude = asset["y"] + spot.dx_longitude = asset["x"] + break + + new_spots.append(spot) + return new_spots