From bc326388ac67459a58ad3acc606e44833c6bb273 Mon Sep 17 00:00:00 2001
From: Ian Renton
Date: Mon, 31 Aug 2026 09:39:05 +0100
Subject: [PATCH] Add support for FEA
---
config-example.yml | 3 +++
core/constants.py | 8 +++++-
providers/sigrefdata/fea.py | 50 +++++++++++++++++++++++++++++++++++++
requirements.txt | 3 ++-
static/apidocs/openapi.yml | 1 +
static/js/ui-ham.js | 3 +++
templates/about.html | 4 +--
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, 83 insertions(+), 19 deletions(-)
create mode 100644 providers/sigrefdata/fea.py
diff --git a/config-example.yml b/config-example.yml
index 151bdf4..50a220f 100644
--- a/config-example.yml
+++ b/config-example.yml
@@ -246,6 +246,9 @@ sig_ref_data_providers:
- class: "DME"
enabled: true
+ - class: "FEA"
+ enabled: true
+
- class: "KRMNPA"
enabled: true
diff --git a/core/constants.py b/core/constants.py
index f767e8f..c025e1f 100644
--- a/core/constants.py
+++ b/core/constants.py
@@ -147,9 +147,15 @@ SIGS = [
SIG(
name="DME",
comment_names=["DME"],
- description="Diplomas de Municipios Españoles",
+ description="Diploma Municipios de España",
ref_regex=r"\d{4,5}",
),
+ SIG(
+ name="FEA",
+ comment_names=["FEA"],
+ description="Diploma Faros de España",
+ ref_regex=r"([DE]|FEA)\-\d{4}(\.\d)?",
+ ),
SIG(
name="DTMBA",
comment_names=["DTMBA"],
diff --git a/providers/sigrefdata/fea.py b/providers/sigrefdata/fea.py
new file mode 100644
index 0000000..186f8c3
--- /dev/null
+++ b/providers/sigrefdata/fea.py
@@ -0,0 +1,50 @@
+from io import BytesIO
+from time import sleep
+
+import pdfplumber
+
+from data.sig_ref import SIGRef
+from providers.sigrefdata.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
+
+
+class FEA(FileDownloadSIGRefDataProvider):
+ """SIG ref data provider for Diploma Faros de España"""
+
+ POLL_INTERVAL_DAYS = 30
+ SIG = "FEA"
+ DATA_URL = "http://ea5ol.net/Lista%20Faros.pdf"
+
+ 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 = []
+
+ # Use PDFPlumber to extract the tables in the PDF
+ with pdfplumber.open(BytesIO(http_response.content)) as pdf:
+ all_rows = []
+
+ for page_number, page in enumerate(pdf.pages, start=1):
+ tables = page.extract_tables()
+
+ for table_number, table in enumerate(tables, start=1):
+ if not table:
+ continue
+
+ rows = [row for row in table if any(cell and cell.strip() for cell in row)]
+ all_rows.extend(rows)
+
+ for row in all_rows:
+ if not "REF" in row[0] and not "\n" in row[0]:
+ new_data.append(SIGRef(sig=self.SIG, id=row[0].strip(), name=row[1].strip(), ref_type="Lighthouse"))
+
+ # 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/requirements.txt b/requirements.txt
index 274f5ed..6a89668 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -20,4 +20,5 @@ geopandas~=0.13.2
simplejson~=4.1.1
cachetools~=7.1.6
fastkml~=1.4.0
-ruff~=0.16.3
\ No newline at end of file
+ruff~=0.16.3
+pdfplumber~=0.11.10
\ No newline at end of file
diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml
index e2d495a..f51fadf 100644
--- a/static/apidocs/openapi.yml
+++ b/static/apidocs/openapi.yml
@@ -852,6 +852,7 @@ components:
- WAB
- WAI
- DME
+ - FEA
- DTMBA
- Toilets
example: POTA
diff --git a/static/js/ui-ham.js b/static/js/ui-ham.js
index cee6cf5..32dd14f 100644
--- a/static/js/ui-ham.js
+++ b/static/js/ui-ham.js
@@ -364,6 +364,7 @@ const SIG_ICONS = {
"WAB": "fa-table-cells-large",
"WAI": "fa-table-cells-large",
"DME": "fa-building",
+ "FEA": "fa-house-flood-water",
"Tiles": "fa-square",
"DTMBA": "fa-masks-theater",
"Toilets": "fa-toilet"
@@ -392,6 +393,8 @@ const SIG_NAMES = {
"WWTOTA": "Towers on the Air",
"WAB": "Worked All Britain",
"WAI": "Worked All Ireland",
+ "DME": "Diploma Municipios de España",
+ "FEA": "Diploma Faros de España",
"Tiles": "Tiles on the Air",
"DTMBA": "Diploma Teatri Musei e Belle Arti",
"TOTA": "Toilets on the Air"
diff --git a/templates/about.html b/templates/about.html
index 767ec08..ab0132c 100644
--- a/templates/about.html
+++ b/templates/about.html
@@ -102,8 +102,8 @@
on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National
Parks Award (KRMNPA), South Australia National Parks and Conservation Parks Award (SANPCPA), Wainwrights on the
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), Diploma
- Teatri Musei e Belle Arti (DTMBA) and Toilets on the Air.
+ 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) and Toilets on the Air.
As of the time of writing in June 2026, 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!