mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-12-15 16:43:38 +00:00
Support BOTA alerts. Closes #58
This commit is contained in:
@@ -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, the UK Packet Repeater Network, and NG3K.
|
||||
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, and NG3K.
|
||||
|
||||

|
||||
|
||||
|
||||
48
alertproviders/bota.py
Normal file
48
alertproviders/bota.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
from bs4 import BeautifulSoup
|
||||
from alertproviders.http_alert_provider import HTTPAlertProvider
|
||||
from core.sig_utils import get_icon_for_sig
|
||||
from data.alert import Alert
|
||||
from data.sig_ref import SIGRef
|
||||
|
||||
|
||||
# Alert provider for Beaches on the Air
|
||||
class BOTA(HTTPAlertProvider):
|
||||
POLL_INTERVAL_SEC = 3600
|
||||
ALERTS_URL = "https://www.beachesontheair.com/"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
|
||||
|
||||
def http_response_to_alerts(self, http_response):
|
||||
new_alerts = []
|
||||
# Find the table of upcoming alerts
|
||||
bs = BeautifulSoup(http_response.content.decode(), features="lxml")
|
||||
tbody = bs.body.find('div', attrs={'class': 'view-activations-public'}).find('table', attrs={'class': 'views-table'}).find('tbody')
|
||||
for row in tbody.find_all('tr'):
|
||||
cells = row.find_all('td')
|
||||
first_cell_text = str(cells[0].find('a').contents[0]).strip()
|
||||
ref_name = first_cell_text.split(" by ")[0]
|
||||
dx_call = str(cells[1].find('a').contents[0]).strip().upper()
|
||||
|
||||
# Get the date, dealing with the fact we get no year so have to figure out if it's last year or next year
|
||||
date_text = str(cells[2].find('span').contents[0]).strip()
|
||||
date_time = datetime.strptime(date_text,"%d %b - %H:%M UTC").replace(tzinfo=pytz.UTC)
|
||||
date_time = date_time.replace(year=datetime.now(pytz.UTC).year)
|
||||
# If this was more than a day ago, activation is actually next year
|
||||
if date_time < datetime.now(pytz.UTC) - timedelta(days=1):
|
||||
date_time = date_time.replace(year=datetime.now(pytz.UTC).year + 1)
|
||||
|
||||
# Convert to our alert format
|
||||
alert = Alert(source=self.name,
|
||||
dx_calls=[dx_call],
|
||||
sig="BOTA",
|
||||
sig_refs=[SIGRef(id=ref_name, name=ref_name, url="https://www.beachesontheair.com/beaches/" + ref_name.lower().replace(" ", "-"))],
|
||||
icon=get_icon_for_sig("BOTA"),
|
||||
start_time=date_time.timestamp(),
|
||||
is_dxpedition=False)
|
||||
|
||||
new_alerts.append(alert)
|
||||
return new_alerts
|
||||
@@ -104,6 +104,10 @@ alert-providers:
|
||||
class: "WOTA"
|
||||
name: "WOTA"
|
||||
enabled: true
|
||||
-
|
||||
class: "BOTA"
|
||||
name: "BOTA"
|
||||
enabled: true
|
||||
-
|
||||
class: "NG3K"
|
||||
name: "NG3K"
|
||||
|
||||
@@ -27,7 +27,8 @@ SIGS = [
|
||||
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="WOTA", description="Wainwrights on the Air", icon="w", ref_regex=r"[A-Z]{3}-[0-9]{2}")
|
||||
SIG(name="WOTA", description="Wainwrights on the Air", icon="w", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
|
||||
SIG(name="BOTA", description="Beaches on the Air", icon="water", ref_regex=None)
|
||||
]
|
||||
|
||||
# Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA".
|
||||
|
||||
@@ -11,4 +11,4 @@ class SIG:
|
||||
# and Field Spotter. Does not include the "fa-" prefix.
|
||||
icon: str
|
||||
# Regex matcher for references, e.g. for POTA r"[A-Z]{2}\-\d+".
|
||||
ref_regex: str
|
||||
ref_regex: str = None
|
||||
@@ -11,4 +11,5 @@ psutil~=7.1.0
|
||||
requests-sse~=0.5.2
|
||||
rss-parser~=2.1.1
|
||||
pyproj~=3.7.2
|
||||
prometheus_client~=0.23.1
|
||||
prometheus_client~=0.23.1
|
||||
beautifulsoup4~=4.14.2
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
|
||||
import aprslib
|
||||
@@ -58,5 +58,5 @@ class APRSIS(SpotProvider):
|
||||
self.submit(spot)
|
||||
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(timezone.utc)
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
logging.debug("Data received from APRS-IS.")
|
||||
@@ -1,17 +1,16 @@
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
import pytz
|
||||
import telnetlib3
|
||||
|
||||
from core.constants import SIGS
|
||||
from core.sig_utils import ANY_SIG_REGEX, ANY_XOTA_SIG_REF_REGEX, get_icon_for_sig, get_ref_regex_for_sig
|
||||
from core.config import SERVER_OWNER_CALLSIGN
|
||||
from core.sig_utils import ANY_SIG_REGEX, get_icon_for_sig, get_ref_regex_for_sig
|
||||
from data.sig_ref import SIGRef
|
||||
from data.spot import Spot
|
||||
from core.config import SERVER_OWNER_CALLSIGN
|
||||
from spotproviders.spot_provider import SpotProvider
|
||||
|
||||
|
||||
@@ -97,7 +96,7 @@ class DXCluster(SpotProvider):
|
||||
self.submit(spot)
|
||||
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(timezone.utc)
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
logging.debug("Data received from DX Cluster " + self.hostname + ".")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
|
||||
import pytz
|
||||
import telnetlib3
|
||||
|
||||
from data.spot import Spot
|
||||
from core.config import SERVER_OWNER_CALLSIGN
|
||||
from data.spot import Spot
|
||||
from spotproviders.spot_provider import SpotProvider
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class RBN(SpotProvider):
|
||||
self.submit(spot)
|
||||
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(timezone.utc)
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
logging.debug("Data received from RBN on port " + str(self.port) + ".")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -74,14 +74,15 @@ class WOTA(HTTPSpotProvider):
|
||||
time=time.timestamp())
|
||||
|
||||
# WOTA name/grid/lat/lon lookup
|
||||
wota_data = self.LIST_CACHE.get(self.LIST_URL, headers=HTTP_HEADERS).json()
|
||||
for feature in wota_data["features"]:
|
||||
if feature["properties"]["wotaId"] == spot.sig_refs[0]:
|
||||
spot.sig_refs[0].name = feature["properties"]["title"]
|
||||
spot.dx_latitude = feature["geometry"]["coordinates"][1]
|
||||
spot.dx_longitude = feature["geometry"]["coordinates"][0]
|
||||
spot.dx_grid = feature["properties"]["qthLocator"]
|
||||
break
|
||||
if ref:
|
||||
wota_data = self.LIST_CACHE.get(self.LIST_URL, headers=HTTP_HEADERS).json()
|
||||
for feature in wota_data["features"]:
|
||||
if feature["properties"]["wotaId"] == ref:
|
||||
spot.sig_refs[0].name = feature["properties"]["title"]
|
||||
spot.dx_latitude = feature["geometry"]["coordinates"][1]
|
||||
spot.dx_longitude = feature["geometry"]["coordinates"][0]
|
||||
spot.dx_grid = feature["properties"]["qthLocator"]
|
||||
break
|
||||
|
||||
new_spots.append(spot)
|
||||
return new_spots
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
<p>In amateur radio terminology, the "DX" contact is the "interesting" one that is using the frequency shown. 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 spotted the "DX" operator. "Modes" are the type of communication they are using. 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.</p>
|
||||
<h4 class="mt-4">What data sources are supported?</h4>
|
||||
<p>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, and the UK Packet Repeater Network.</p>
|
||||
<p>Spothole can retrieve alerts from: NG3K, POTA, SOTA, WWFF, Parks 'n' Peaks, and WOTA.</p>
|
||||
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, IOTA, MOTS, ARLHS, ILLW, SIOTA, WCA, ZLOTA, KRMNPA, WOTA, WAB & WAI.</p>
|
||||
<p>Spothole can retrieve alerts from: NG3K, POTA, SOTA, WWFF, Parks 'n' Peaks, WOTA and BOTA.</p>
|
||||
<p>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.</p>
|
||||
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, IOTA, MOTS, ARLHS, ILLW, SIOTA, WCA, ZLOTA, KRMNPA, WOTA, BOTA, WAB & WAI.</p>
|
||||
<h4 class="mt-4">How is this better than DXheat, DXsummit, POTA's own website, etc?</h4>
|
||||
<p>It's probably not? But it's nice to have choice.</p>
|
||||
<p>I think it's got two key advantages over those sites:</p>
|
||||
|
||||
@@ -62,6 +62,7 @@ paths:
|
||||
- ParksNPeaks
|
||||
- ZLOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- Cluster
|
||||
- RBN
|
||||
- APRS-IS
|
||||
@@ -87,6 +88,7 @@ paths:
|
||||
- ZLOTA
|
||||
- IOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- WAB
|
||||
- WAI
|
||||
- name: needs_sig
|
||||
@@ -284,6 +286,7 @@ paths:
|
||||
- ParksNPeaks
|
||||
- ZLOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- Cluster
|
||||
- RBN
|
||||
- APRS-IS
|
||||
@@ -309,6 +312,7 @@ paths:
|
||||
- ZLOTA
|
||||
- IOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- WAB
|
||||
- WAI
|
||||
- name: dx_continent
|
||||
@@ -768,6 +772,7 @@ components:
|
||||
- ZLOTA
|
||||
- IOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- WAB
|
||||
- WAI
|
||||
example: POTA
|
||||
@@ -921,6 +926,7 @@ components:
|
||||
- ZLOTA
|
||||
- IOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- WAB
|
||||
- WAI
|
||||
example: POTA
|
||||
@@ -950,6 +956,7 @@ components:
|
||||
- ParksNPeaks
|
||||
- ZLOTA
|
||||
- WOTA
|
||||
- BOTA
|
||||
- Cluster
|
||||
- RBN
|
||||
- APRS-IS
|
||||
|
||||
@@ -213,9 +213,17 @@ function addAlertRowsToTable(tbody, alerts) {
|
||||
}
|
||||
|
||||
// Format sig_refs
|
||||
var sig_refs = ""
|
||||
if (a["sig_refs"]) {
|
||||
sig_refs = a["sig_refs"].map(a => `<span class='nowrap'>${a}</span>`).join(", ");
|
||||
var sig_refs = "";
|
||||
if (a["sig_refs"] != null) {
|
||||
var items = []
|
||||
for (var i = 0; i < a["sig_refs"].length; i++) {
|
||||
if (a["sig_refs"][i]["url"] != null) {
|
||||
items[i] = `<a href='${a["sig_refs"][i]["url"]}' title='${a["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${a["sig_refs"][i]["id"]}</a>`
|
||||
} else {
|
||||
items[i] = `${a["sig_refs"][i]["id"]}`
|
||||
}
|
||||
}
|
||||
sig_refs = items.join(", ");
|
||||
}
|
||||
|
||||
// Populate the row
|
||||
|
||||
@@ -109,7 +109,11 @@ function getTooltipText(s) {
|
||||
if (s["sig_refs"] != null) {
|
||||
var items = []
|
||||
for (var i = 0; i < s["sig_refs"].length; i++) {
|
||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
||||
if (s["sig_refs"][i]["url"] != null) {
|
||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
||||
} else {
|
||||
items[i] = `${s["sig_refs"][i]["id"]}`
|
||||
}
|
||||
}
|
||||
sig_refs = items.join(", ");
|
||||
}
|
||||
|
||||
@@ -172,7 +172,11 @@ function updateTable() {
|
||||
if (s["sig_refs"] != null) {
|
||||
var items = []
|
||||
for (var i = 0; i < s["sig_refs"].length; i++) {
|
||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
||||
if (s["sig_refs"][i]["url"] != null) {
|
||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
||||
} else {
|
||||
items[i] = `${s["sig_refs"][i]["id"]}`
|
||||
}
|
||||
}
|
||||
sig_refs = items.join(", ");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user