From a866d41aa7d89331a5cb2e88b03fe1a7bfd5a209 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Thu, 9 Oct 2025 21:25:01 +0100 Subject: [PATCH] ZLOTA support + misc changes --- .gitignore | 1 + core/utils.py | 21 +++++++++++++++------ spotproviders/parksnpeaks.py | 27 +++++++++++++++++++++------ views/webpage_alerts.tpl | 2 +- views/webpage_spots.tpl | 2 +- webassets/js/spots.js | 17 +++++++++++++++-- 6 files changed, 54 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 63419fa..862809d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__ /gma_ref_info_cache.sqlite /config.yml /siota_data_cache.sqlite +/zlota_data_cache.sqlite diff --git a/core/utils.py b/core/utils.py index d23ceb3..446b5c3 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,5 +1,4 @@ import logging -from datetime import datetime from diskcache import Cache from pyhamtools import LookupLib, Callinfo @@ -48,35 +47,45 @@ def infer_band_from_freq(freq): # Infer a country name from a callsign def infer_country_from_callsign(call): try: - return CALL_INFO_BASIC.get_country_name(call) + # Get base callsign, assuming this will be the longest of any /-separated sections. + base_call = max(call.split("/"), key=len) + return CALL_INFO_BASIC.get_country_name(base_call) except KeyError as e: return None # Infer a DXCC ID from a callsign def infer_dxcc_id_from_callsign(call): try: - return CALL_INFO_BASIC.get_adif_id(call) + # Get base callsign, assuming this will be the longest of any /-separated sections. + base_call = max(call.split("/"), key=len) + return CALL_INFO_BASIC.get_adif_id(base_call) except KeyError as e: return None # Infer a continent shortcode from a callsign def infer_continent_from_callsign(call): try: - return CALL_INFO_BASIC.get_continent(call) + # Get base callsign, assuming this will be the longest of any /-separated sections. + base_call = max(call.split("/"), key=len) + return CALL_INFO_BASIC.get_continent(base_call) except KeyError as e: return None # Infer a CQ zone from a callsign def infer_cq_zone_from_callsign(call): try: - return CALL_INFO_BASIC.get_cqz(call) + # Get base callsign, assuming this will be the longest of any /-separated sections. + base_call = max(call.split("/"), key=len) + return CALL_INFO_BASIC.get_cqz(base_call) except KeyError as e: return None # Infer a ITU zone from a callsign def infer_itu_zone_from_callsign(call): try: - return CALL_INFO_BASIC.get_ituz(call) + # Get base callsign, assuming this will be the longest of any /-separated sections. + base_call = max(call.split("/"), key=len) + return CALL_INFO_BASIC.get_ituz(base_call) except KeyError as e: return None diff --git a/spotproviders/parksnpeaks.py b/spotproviders/parksnpeaks.py index f75076b..96a2289 100644 --- a/spotproviders/parksnpeaks.py +++ b/spotproviders/parksnpeaks.py @@ -3,7 +3,6 @@ import logging from datetime import datetime, timedelta import pytz -import requests from requests_cache import CachedSession from data.spot import Spot @@ -14,9 +13,12 @@ from spotproviders.http_spot_provider import HTTPSpotProvider class ParksNPeaks(HTTPSpotProvider): POLL_INTERVAL_SEC = 120 SPOTS_URL = "https://www.parksnpeaks.org/api/ALL" - SIOTA_CSV_URL = "https://www.silosontheair.com/data/silos.csv" - SIOTA_CSV_CACHE_TIME_DAYS = 30 - SIOTA_CSV_CACHE = CachedSession("siota_data_cache", expire_after=timedelta(days=SIOTA_CSV_CACHE_TIME_DAYS)) + SIOTA_LIST_URL = "https://www.silosontheair.com/data/silos.csv" + SIOTA_LIST_CACHE_TIME_DAYS = 30 + SIOTA_LIST_CACHE = CachedSession("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("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) @@ -43,6 +45,8 @@ class ParksNPeaks(HTTPSpotProvider): # PNP supports a bunch of programs which should have different icons if spot.sig == "SiOTA": spot.icon = "wheat-awn" + elif spot.sig == "ZLOTA": + spot.icon = "kiwi-bird" elif spot.sig in ["POTA", "SOTA", "WWFF"]: # Don't care about an icon as this will be rejected anyway, we have better data from POTA/SOTA/WWFF direct spot.icon = "" @@ -54,16 +58,27 @@ class ParksNPeaks(HTTPSpotProvider): # SiOTA lat/lon/grid lookup if spot.sig == "SiOTA": - siota_csv_data = self.SIOTA_CSV_CACHE.get(self.SIOTA_CSV_URL, headers=self.HTTP_HEADERS) + siota_csv_data = self.SIOTA_LIST_CACHE.get(self.SIOTA_LIST_URL, headers=self.HTTP_HEADERS) siota_dr = csv.DictReader(siota_csv_data.content.decode().splitlines()) for row in siota_dr: if row["SILO_CODE"] == spot.sig_refs[0]: - spot.dx_country = row["COUNTRY"] spot.latitude = float(row["LAT"]) spot.longitude = float(row["LON"]) spot.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=self.HTTP_HEADERS).json() + for asset in zlota_data: + if asset["code"] == spot.sig_refs[0]: + spot.sig_refs_names = [asset["name"]] + spot.latitude = asset["y"] + spot.longitude = asset["x"] + # Junk the "DE call", PNP always returns "ZLOTA" as the spotter for ZLOTA spots + spot.de_call = None + break + # 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"]: diff --git a/views/webpage_alerts.tpl b/views/webpage_alerts.tpl index fc5b3ea..8b11787 100644 --- a/views/webpage_alerts.tpl +++ b/views/webpage_alerts.tpl @@ -7,7 +7,7 @@

- +

diff --git a/views/webpage_spots.tpl b/views/webpage_spots.tpl index 8ee7450..9b0357d 100644 --- a/views/webpage_spots.tpl +++ b/views/webpage_spots.tpl @@ -22,7 +22,7 @@

- +

diff --git a/webassets/js/spots.js b/webassets/js/spots.js index 659fe04..068b463 100644 --- a/webassets/js/spots.js +++ b/webassets/js/spots.js @@ -111,6 +111,12 @@ function updateTable() { sig_refs = s["sig_refs"].map(s => `${s}`).join(", "); } + // Format sig_refs title + var sig_refs_title_string = ""; + if (s["sig_refs_names"]) { + sig_refs_title_string = " title=\"" + s["sig_refs_names"].join(", ") + "\""; + } + // Format DE flag var de_flag = ""; if (s["de_flag"] && s["de_flag"] != null && s["de_flag"] != "") { @@ -123,6 +129,13 @@ function updateTable() { de_country = "Unknown or not a country"; } + // Format de call + var de_call = s["de_call"]; + if (de_call == null) { + de_call = ""; + de_flag = ""; + } + // CSS doesn't like classes with decimal points in, so we need to replace that in the same way as when we originally // queried the options endpoint and set our CSS. var cssFormattedBandName = s['band'] ? s['band'].replace('.', 'p') : "unknown"; @@ -135,8 +148,8 @@ function updateTable() { $tr.append(`${mode_string}`); $tr.append(`${commentText}`); $tr.append(` ${sigSourceText}`); - $tr.append(`${sig_refs}`); - $tr.append(`${de_flag}${s["de_call"]}`); + $tr.append(`${sig_refs}`); + $tr.append(`${de_flag}${de_call}`); table.find('tbody').append($tr); // Second row for mobile view only, containing source, ref & comment