From 1b62d4742bac761814c939617a5aedffc02cbc20 Mon Sep 17 00:00:00 2001
From: Ian Renton
Date: Mon, 31 Aug 2026 21:54:13 +0100
Subject: [PATCH] Support PGA. Closes #135
---
config-example.yml | 3 ++
core/constants.py | 9 ++++++
core/utils.py | 4 +--
providers/sigrefdata/pga.py | 57 +++++++++++++++++++++++++++++++++++++
spothole.py | 4 +++
static/apidocs/openapi.yml | 4 ++-
templates/about.html | 2 +-
templates/add_spot.html | 2 +-
templates/alerts.html | 2 +-
templates/bands.html | 4 +--
templates/base.html | 10 +++----
templates/conditions.html | 2 +-
templates/map.html | 4 +--
templates/spots.html | 4 +--
templates/status.html | 2 +-
15 files changed, 94 insertions(+), 19 deletions(-)
create mode 100644 providers/sigrefdata/pga.py
diff --git a/config-example.yml b/config-example.yml
index 0c5688f..e9be2ab 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -261,6 +261,9 @@ sig_ref_data_providers:
- class: "COTA"
enabled: true
+ - class: "PGA"
+ enabled: true
+
- class: "Toilets"
enabled: true
diff --git a/core/constants.py b/core/constants.py
index 7397c6f..0bcf39a 100644
--- a/core/constants.py
+++ b/core/constants.py
@@ -242,6 +242,15 @@ SIGS = [
region_flag="🇩🇪",
refs_globally_unique=False
),
+ SIG(
+ name="PGA",
+ comment_names=["PGA"],
+ description="Polish Gmina Award",
+ ref_regex=r"[A-Z]{2}[0-9]{2}",
+ icon="fa-g",
+ region_flag="🇵🇱",
+ refs_globally_unique=False
+ ),
SIG(
name="Toilets",
comment_names=[],
diff --git a/core/utils.py b/core/utils.py
index 11ca7a5..f0a4ab2 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -31,10 +31,10 @@ def infer_mode_from_comment(comment):
"""Infer a mode from the comment"""
for mode in ALL_MODES:
- if re.match(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE):
+ if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE):
return mode
for mode in MODE_ALIASES:
- if re.match(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE):
+ if re.search(r"(^|\W)" + mode + r"($|\W)", comment, re.IGNORECASE):
return MODE_ALIASES[mode]
return None
diff --git a/providers/sigrefdata/pga.py b/providers/sigrefdata/pga.py
new file mode 100644
index 0000000..d20bb04
--- /dev/null
+++ b/providers/sigrefdata/pga.py
@@ -0,0 +1,57 @@
+from time import sleep
+
+from bs4 import BeautifulSoup
+
+from data.sig_ref import SIGRef
+from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
+
+
+class PGA(FileDownloadSIGRefDataProvider):
+ """SIG ref data provider for Polish Gmina Award"""
+
+ POLL_INTERVAL_DAYS = 30
+ SIG = "PGA"
+ DATA_URL = "http://www.spga.pl/lista_pga2.php"
+
+ def __init__(self, provider_config):
+ super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
+
+ def _http_response_to_data(self, http_response):
+ new_data = []
+ soup = BeautifulSoup(http_response.text, "html.parser")
+
+ # Iterate through tables in the page
+ for table in soup.find_all("table"):
+ header_cells = table.find_all(["th", "td"], limit=10)
+ header_texts = [c.get_text(strip=True) for c in header_cells]
+
+ # If it has "PGA" and "Nazwa" in the header, it's the main data table
+ if any("PGA" in t for t in header_texts) and any("Nazwa" in t for t in header_texts):
+ # Iterate through all rows except the first
+ rows = table.find_all("tr")
+ for row in rows[1:]:
+ cells = row.find_all("td")
+ ref_id = cells[0].get_text(strip=True)
+ name = cells[1].get_text(strip=True)
+ if not ref_id:
+ continue
+
+ new_data.append(
+ SIGRef(
+ sig=self.SIG,
+ id=ref_id,
+ name=name,
+ ref_type="Region",
+ )
+ )
+
+ # Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest
+ # of the data in this case
+ if self._stop_event.is_set():
+ break
+
+ # Very short pause. This will extend the time to handle sig refs by a few seconds but will ensure some time
+ # is available for other threads e.g. the web server.
+ sleep(0.001)
+
+ return new_data
diff --git a/spothole.py b/spothole.py
index 64ef753..9899c98 100644
--- a/spothole.py
+++ b/spothole.py
@@ -10,6 +10,7 @@ from core.constants import SOFTWARE_VERSION
from core.data_providers import DATA_PROVIDERS
from core.data_store import DATA_STORE
from core.status_reporter import StatusReporter
+from core.utils import infer_mode_from_comment
from server.webserver import WEB_SERVER
logger = logging.getLogger(__name__)
@@ -28,6 +29,9 @@ def shutdown(_signum=None, _frame=None):
# Main function
if __name__ == "__main__":
+ result = infer_mode_from_comment("Test FT8")
+ print(result)
+
# Set up logging
root = logging.getLogger()
root.setLevel(LOG_LEVEL)
diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml
index 6bf69a6..4d2769e 100644
--- a/static/apidocs/openapi.yml
+++ b/static/apidocs/openapi.yml
@@ -17,7 +17,7 @@ info:
### 2.1
- * Added DTMBA, FEA and BIWOTA SIGs
+ * Added DTMBA, FEA, BIWOTA, COTA & PGA SIGs
* Removed the distinction between LSB & USB (both will now show as SSB) and between the various digital voice modes, which will now show as DV.
* Added `icon`, `region_flag` and `refs_globally_unique` to SIG information
@@ -861,6 +861,8 @@ components:
- FEA
- DTMBA
- BIWOTA
+ - COTA
+ - PGA
- Toilets
example: POTA
diff --git a/templates/about.html b/templates/about.html
index a387a36..3f2f346 100644
--- a/templates/about.html
+++ b/templates/about.html
@@ -104,7 +104,7 @@
Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air, Tiles on
the Air, Worked All Britain (WAB), Worked All Ireland (WAI), el Diploma Municipios de España (DME), el Diploma
Faros de España (FEA), il Diploma Teatri Musei e Belle Arti (DTMBA), British Inland Waterways on the Air
- (BIWOTA) and Toilets on the Air.
+ (BIWOTA), Castles on the Air (COTA), Polish Gmina Award (PGA), and Toilets on the Air.
As of the time of writing in August 2026, I think Spothole captures most outdoor radio programmes that have a
defined, downloadable reference list, and almost certainly those that have a spotting/alerting API. If you know
of one I've missed, please let me know!
diff --git a/templates/add_spot.html b/templates/add_spot.html
index 6f0eb8c..3c00a89 100644
--- a/templates/add_spot.html
+++ b/templates/add_spot.html
@@ -76,7 +76,7 @@
-
+
diff --git a/templates/alerts.html b/templates/alerts.html
index 02d39be..57061bd 100644
--- a/templates/alerts.html
+++ b/templates/alerts.html
@@ -84,7 +84,7 @@
-
+
diff --git a/templates/bands.html b/templates/bands.html
index 39ce6c5..b4a7bd9 100644
--- a/templates/bands.html
+++ b/templates/bands.html
@@ -76,8 +76,8 @@
-
-
+
+
diff --git a/templates/base.html b/templates/base.html
index 1d2a93e..2e3fd20 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %}
{% block head_extra %}
-
+
@@ -15,10 +15,10 @@
window.fetchEventSource = fetchEventSource;
-
-
-
-
+
+
+
+
{% end %}
{% block body %}
diff --git a/templates/conditions.html b/templates/conditions.html
index 49fef8a..af6086f 100644
--- a/templates/conditions.html
+++ b/templates/conditions.html
@@ -284,7 +284,7 @@
-
+
diff --git a/templates/map.html b/templates/map.html
index 104a2f9..d28d2a8 100644
--- a/templates/map.html
+++ b/templates/map.html
@@ -113,8 +113,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
-
-
+
+
diff --git a/templates/spots.html b/templates/spots.html
index 6a747a9..94a5ef1 100644
--- a/templates/spots.html
+++ b/templates/spots.html
@@ -113,8 +113,8 @@
-
-
+
+
diff --git a/templates/status.html b/templates/status.html
index 7a1c69a..29310e3 100644
--- a/templates/status.html
+++ b/templates/status.html
@@ -86,7 +86,7 @@
-
+