Use ruff linter to fix issues and provide consistent formatting

This commit is contained in:
Ian Renton
2026-08-15 08:25:54 +01:00
parent 7391c28cd0
commit af3f82c14d
121 changed files with 1989 additions and 996 deletions
+11 -10
View File
@@ -43,16 +43,17 @@ class APRSIS(SpotProvider):
via_parts = str(data["via"]).split("-")
de_call = via_parts[0].upper()
de_ssid = via_parts[1].upper() if len(via_parts) > 1 else None
spot = Spot(source="APRS-IS",
dx_call=dx_call,
dx_ssid=dx_ssid,
de_call=de_call,
de_ssid=de_ssid,
comment=str(data["comment"]) if "comment" in data else None,
dx_latitude=float(data["latitude"]) if "latitude" in data else None,
dx_longitude=float(data["longitude"]) if "longitude" in data else None,
time=datetime.now(
pytz.UTC).timestamp()) # APRS-IS spots are live so we can assume spot time is "now"
spot = Spot(
source="APRS-IS",
dx_call=dx_call,
dx_ssid=dx_ssid,
de_call=de_call,
de_ssid=de_ssid,
comment=str(data["comment"]) if "comment" in data else None,
dx_latitude=float(data["latitude"]) if "latitude" in data else None,
dx_longitude=float(data["longitude"]) if "longitude" in data else None,
time=datetime.now(pytz.UTC).timestamp(),
) # APRS-IS spots are live so we can assume spot time is "now"
# Add to our list
self._submit(spot)
+23 -13
View File
@@ -18,10 +18,12 @@ class DXCluster(SpotProvider):
_LINE_PATTERN_EXCLUDE_RBN = re.compile(
r"^DX de ([a-z0-9/]+):\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)",
re.IGNORECASE)
re.IGNORECASE,
)
_LINE_PATTERN_ALLOW_RBN = re.compile(
r"^DX de ([a-z0-9/]+)-?#?:\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)",
re.IGNORECASE)
re.IGNORECASE,
)
def __init__(self, provider_config):
"""Constructor requires hostname and port"""
@@ -31,10 +33,13 @@ class DXCluster(SpotProvider):
self._hostname = provider_config["host"]
self._port = provider_config["port"]
self._login_prompt = provider_config["login_prompt"] if "login_prompt" in provider_config else "login:"
self._login_callsign = provider_config[
"login_callsign"] if "login_callsign" in provider_config else SERVER_OWNER_CALLSIGN
self._login_callsign = (
provider_config["login_callsign"] if "login_callsign" in provider_config else SERVER_OWNER_CALLSIGN
)
self._allow_rbn_spots = provider_config["allow_rbn_spots"] if "allow_rbn_spots" in provider_config else False
self._spot_line_pattern = self._LINE_PATTERN_ALLOW_RBN if self._allow_rbn_spots else self._LINE_PATTERN_EXCLUDE_RBN
self._spot_line_pattern = (
self._LINE_PATTERN_ALLOW_RBN if self._allow_rbn_spots else self._LINE_PATTERN_EXCLUDE_RBN
)
self._telnet = None
self._thread = Thread(target=self._handle, name=f"DXClusterSpotProvider-{self.name}")
self._thread.daemon = True
@@ -77,14 +82,19 @@ class DXCluster(SpotProvider):
match = self._spot_line_pattern.match(telnet_output.decode("latin-1"))
if match:
spot_time = datetime.strptime(match.group(5), "%H%MZ")
spot_datetime = datetime.combine(datetime.now(pytz.UTC).date(), spot_time.time(),
tzinfo=pytz.UTC)
spot = Spot(source=self.name,
dx_call=match.group(3),
de_call=match.group(1),
freq=float(match.group(2)) * 1000,
comment=match.group(4).strip(),
time=spot_datetime.timestamp())
spot_datetime = datetime.combine(
datetime.now(pytz.UTC).date(),
spot_time.time(),
tzinfo=pytz.UTC,
)
spot = Spot(
source=self.name,
dx_call=match.group(3),
de_call=match.group(1),
freq=float(match.group(2)) * 1000,
comment=match.group(4).strip(),
time=spot_datetime.timestamp(),
)
# Add to our list
self._submit(spot)
+64 -29
View File
@@ -27,7 +27,12 @@ class GMA(HTTPSpotProvider):
logging.warning("GMA spot provider configured but no api key was provided, this API will not be queried.")
self._url_data_cache = URLDataCache("GMA")
super().__init__("GMA", provider_config, f"{self.SPOTS_URL}?key={self._api_key}", self.POLL_INTERVAL_SEC)
super().__init__(
"GMA",
provider_config,
f"{self.SPOTS_URL}?key={self._api_key}",
self.POLL_INTERVAL_SEC,
)
def _http_response_to_spots(self, http_response):
new_spots = []
@@ -41,43 +46,67 @@ class GMA(HTTPSpotProvider):
# Seen some real janky times from GMA, if we don't understand it just ignore this spot
try:
time = datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M").replace(
tzinfo=pytz.UTC).timestamp()
time = (
datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M")
.replace(tzinfo=pytz.UTC)
.timestamp()
)
except ValueError:
continue
spot = Spot(source=self.name,
dx_call=source_spot["ACTIVATOR"].upper(),
de_call=source_spot["SPOTTER"].upper(),
# Seen GMA spots with no frequency or with "QRT" in this field
freq=float(source_spot["QRG"]) * 1000 if (
source_spot["QRG"] != "" and source_spot["QRG"] != "QRT") else None,
# Filter out some weird mode strings
mode=source_spot["MODE"].upper() if "<>" not in source_spot["MODE"] else None,
comment=source_spot["TEXT"],
sig_refs=[SIGRef(id=source_spot["REF"], sig="", name=source_spot["NAME"], latitude=lat,
longitude=lon)],
time=time,
dx_latitude=lat,
dx_longitude=lon,
qrt=source_spot["QRG"] == "QRT")
spot = Spot(
source=self.name,
dx_call=source_spot["ACTIVATOR"].upper(),
de_call=source_spot["SPOTTER"].upper(),
# Seen GMA spots with no frequency or with "QRT" in this field
freq=float(source_spot["QRG"]) * 1000
if (source_spot["QRG"] != "" and source_spot["QRG"] != "QRT")
else None,
# Filter out some weird mode strings
mode=source_spot["MODE"].upper() if "<>" not in source_spot["MODE"] else None,
comment=source_spot["TEXT"],
sig_refs=[
SIGRef(
id=source_spot["REF"],
sig="",
name=source_spot["NAME"],
latitude=lat,
longitude=lon,
)
],
time=time,
dx_latitude=lat,
dx_longitude=lon,
qrt=source_spot["QRG"] == "QRT",
)
# GMA doesn't give what programme (SIG) the reference is for until we separately look it up.
if "REF" in source_spot:
try:
ref_response = self._url_data_cache.get(self.REF_INFO_URL_ROOT + source_spot["REF"],
headers=HTTP_HEADERS)
ref_response = self._url_data_cache.get(
self.REF_INFO_URL_ROOT + source_spot["REF"],
headers=HTTP_HEADERS,
)
# Sometimes this is blank even if it's a 200 response, so handle that
if ref_response.ok and ref_response.text is not None and ref_response.text != "" and ref_response.text != "\n":
if (
ref_response.ok
and ref_response.text is not None
and ref_response.text != ""
and ref_response.text != "\n"
):
ref_info = ref_response.json()
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. POTA and WWFF
# spots come through with reftype=POTA or reftype=WWFF. SOTA is harder to figure out because both SOTA
# and GMA summits come through with reftype=Summit, so we must check for the presence of a "sota" entry
# to determine if it's a SOTA summit.
if spot.sig_refs and "reftype" in ref_info and ref_info["reftype"] not in ["POTA",
"WWFF"] and (
ref_info["reftype"] != "Summit" or "sota" not in ref_info or ref_info[
"sota"] == ""):
if (
spot.sig_refs
and "reftype" in ref_info
and ref_info["reftype"] not in ["POTA", "WWFF"]
and (
ref_info["reftype"] != "Summit" or "sota" not in ref_info or ref_info["sota"] == ""
)
):
match ref_info["reftype"]:
case "Summit":
spot.sig_refs[0].sig = "GMA"
@@ -98,7 +127,9 @@ class GMA(HTTPSpotProvider):
spot.sig_refs[0].sig = "MOTA"
spot.sig = "MOTA"
case _:
logging.warning(f"GMA spot found with ref type {ref_info['reftype']}, developer needs to add support for this!")
logging.warning(
f"GMA spot found with ref type {ref_info['reftype']}, developer needs to add support for this!"
)
spot.sig_refs[0].sig = ref_info["reftype"]
spot.sig = ref_info["reftype"]
@@ -109,12 +140,16 @@ class GMA(HTTPSpotProvider):
elif not ref_response.from_cache:
if not ref_response.ok:
logging.warning(
f"HTTP {ref_response.status_code} when looking up GMA ref {source_spot['REF']}")
f"HTTP {ref_response.status_code} when looking up GMA ref {source_spot['REF']}"
)
else:
logging.warning(
f"GMA API returned a malformed response when looking up ref {source_spot['REF']}")
f"GMA API returned a malformed response when looking up ref {source_spot['REF']}"
)
except:
logging.exception(f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, ignoring this spot for now")
logging.exception(
f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, ignoring this spot for now"
)
else:
logging.warning(f"The GMA API returned an unexpected response (HTTP {http_response.status_code}).")
+24 -14
View File
@@ -4,7 +4,7 @@ from datetime import datetime
import pytz
import requests
from requests.exceptions import ConnectionError, ReadTimeout, ConnectTimeout
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
from core.constants import HTTP_HEADERS
from data.sig_ref import SIGRef
@@ -52,25 +52,35 @@ class HEMA(HTTPSpotProvider):
continue
# Convert to our spot format
spot = Spot(source=self.name,
dx_call=spot_items[2].upper(),
de_call=spotter_comment_match.group(1).upper(),
freq=float(freq_mode_match.group(1)) * 1000000,
mode=freq_mode_match.group(2).upper(),
comment=spotter_comment_match.group(2),
spot = Spot(
source=self.name,
dx_call=spot_items[2].upper(),
de_call=spotter_comment_match.group(1).upper(),
freq=float(freq_mode_match.group(1)) * 1000000,
mode=freq_mode_match.group(2).upper(),
comment=spotter_comment_match.group(2),
sig="HEMA",
sig_refs=[
SIGRef(
id=spot_items[3].upper(),
sig="HEMA",
sig_refs=[SIGRef(id=spot_items[3].upper(), sig="HEMA", name=spot_items[4],
latitude=float(spot_items[7]), longitude=float(spot_items[8]))],
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(
tzinfo=pytz.UTC).timestamp(),
dx_latitude=float(spot_items[7]),
dx_longitude=float(spot_items[8]))
name=spot_items[4],
latitude=float(spot_items[7]),
longitude=float(spot_items[8]),
)
],
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M")
.replace(tzinfo=pytz.UTC)
.timestamp(),
dx_latitude=float(spot_items[7]),
dx_longitude=float(spot_items[8]),
)
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other
# code will do that for us.
new_spots.append(spot)
except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when accessing HEMA spots API.")
logging.warning("Timeout when accessing HEMA spots API.")
except ConnectionError:
logging.warning("Connection error when accessing HEMA spots API.")
return new_spots
+1 -1
View File
@@ -1,6 +1,6 @@
import logging
from datetime import datetime
from threading import Thread, Event
from threading import Event, Thread
import pytz
import requests
+17 -9
View File
@@ -25,16 +25,24 @@ class LLOTA(HTTPSpotProvider):
comment = str(source_spot["history"][-1]["comment"])
spotter = str(source_spot["history"][-1]["spotter_callsign"])
# Convert to our spot format
spot = Spot(source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["callsign"].upper(),
de_call=spotter.upper() if spotter else None,
freq=float(source_spot["frequency"]) * 1000000,
mode=source_spot["mode"].upper(),
comment=comment,
spot = Spot(
source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["callsign"].upper(),
de_call=spotter.upper() if spotter else None,
freq=float(source_spot["frequency"]) * 1000000,
mode=source_spot["mode"].upper(),
comment=comment,
sig="LLOTA",
sig_refs=[
SIGRef(
id=source_spot["reference"],
sig="LLOTA",
sig_refs=[SIGRef(id=source_spot["reference"], sig="LLOTA", name=source_spot["reference_name"])],
time=datetime.fromisoformat(source_spot["updated_at"].replace("Z", "+00:00")).timestamp())
name=source_spot["reference_name"],
)
],
time=datetime.fromisoformat(source_spot["updated_at"].replace("Z", "+00:00")).timestamp(),
)
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.
+46 -16
View File
@@ -18,7 +18,17 @@ class ParksNPeaks(HTTPSpotProvider):
SPOTS_URL = "https://www.parksnpeaks.org/api/ALL"
SUBMIT_URL = "https://www.parksnpeaks.org/api/SPOT/"
SIOTA_LIST_URL = "https://www.silosontheair.com/data/silos.csv"
SUBMITTABLE_SIGS = ["POTA", "SOTA", "WWFF", "HEMA", "WOTA", "ZLOTA", "SIOTA", "KRMNPA", "SANPCPA"]
SUBMITTABLE_SIGS = [
"POTA",
"SOTA",
"WWFF",
"HEMA",
"WOTA",
"ZLOTA",
"SIOTA",
"KRMNPA",
"SANPCPA",
]
def __init__(self, provider_config):
super().__init__("ParksNPeaks", provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
@@ -29,18 +39,23 @@ class ParksNPeaks(HTTPSpotProvider):
if http_response and http_response != "":
for source_spot in http_response.json():
# Convert to our spot format
spot = Spot(source=self.name,
source_id=source_spot["actID"],
dx_call=source_spot["actCallsign"].upper(),
de_call=source_spot["actSpoter"].upper() if source_spot["actSpoter"] != "" else None,
# typo exists in API
freq=float(source_spot["actFreq"].replace(",", "").replace("+-", "")
.replace("+/-", "").strip()) * 1000000 if (source_spot["actFreq"] != "") else None,
# Seen PNP spots with empty frequency, and with comma-separated thousands digits
mode=source_spot["actMode"].upper(),
comment=source_spot["actComments"],
time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S").replace(
tzinfo=pytz.UTC).timestamp())
spot = Spot(
source=self.name,
source_id=source_spot["actID"],
dx_call=source_spot["actCallsign"].upper(),
de_call=source_spot["actSpoter"].upper() if source_spot["actSpoter"] != "" else None,
# typo exists in API
freq=float(source_spot["actFreq"].replace(",", "").replace("+-", "").replace("+/-", "").strip())
* 1000000
if (source_spot["actFreq"] != "")
else None,
# Seen PNP spots with empty frequency, and with comma-separated thousands digits
mode=source_spot["actMode"].upper(),
comment=source_spot["actComments"],
time=datetime.strptime(source_spot["actTime"], "%Y-%m-%d %H:%M:%S")
.replace(tzinfo=pytz.UTC)
.timestamp(),
)
# 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 or "")
@@ -53,7 +68,12 @@ class ParksNPeaks(HTTPSpotProvider):
sig_ref = source_spot["actSiteID"]
if sig and sig != "" and sig != "QRP" and sig_ref and sig_ref != "":
spot.sig = sig
sig_refs = [SIGRef(id=source_spot["actSiteID"], sig=source_spot["actClass"].upper())]
sig_refs = [
SIGRef(
id=source_spot["actSiteID"],
sig=source_spot["actClass"].upper(),
)
]
spot.sig_refs = sig_refs
# Free text location is not present in all spots, so only add it if it's set
@@ -61,7 +81,16 @@ class ParksNPeaks(HTTPSpotProvider):
sig_refs[0].name = source_spot["actLocation"]
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
if sig not in ["POTA", "SOTA", "WWFF", "SIOTA", "ZLOTA", "KRMNPA", "SANPCPA", "LLOTA"]:
if sig not in [
"POTA",
"SOTA",
"WWFF",
"SIOTA",
"ZLOTA",
"KRMNPA",
"SANPCPA",
"LLOTA",
]:
logging.warning(f"PNP spot found with sig {sig}, developer needs to add support for this!")
# Add new spot to the list
@@ -77,7 +106,8 @@ class ParksNPeaks(HTTPSpotProvider):
api_key = credentials.get("api_key", "")
if not user_id or not api_key:
raise ValueError(
"Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account.")
"Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account."
)
sig_ref = spot.sig_refs[0].id if spot.sig_refs else ""
body = {
"actClass": spot.sig or "",
+24 -14
View File
@@ -24,21 +24,31 @@ class POTA(HTTPSpotProvider):
# Iterate through source data
for source_spot in http_response.json():
# Convert to our spot format
spot = Spot(source=self.name,
source_id=source_spot["spotId"],
dx_call=source_spot["activator"].upper(),
de_call=source_spot["spotter"].upper(),
freq=float(source_spot["frequency"]) * 1000,
mode=source_spot["mode"].upper(),
comment=source_spot["comments"],
spot = Spot(
source=self.name,
source_id=source_spot["spotId"],
dx_call=source_spot["activator"].upper(),
de_call=source_spot["spotter"].upper(),
freq=float(source_spot["frequency"]) * 1000,
mode=source_spot["mode"].upper(),
comment=source_spot["comments"],
sig="POTA",
sig_refs=[
SIGRef(
id=source_spot["reference"],
sig="POTA",
sig_refs=[SIGRef(id=source_spot["reference"], sig="POTA", name=source_spot["name"],
latitude=source_spot["latitude"], longitude=source_spot["longitude"])],
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S").replace(
tzinfo=pytz.UTC).timestamp(),
dx_grid=source_spot["grid6"],
dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"])
name=source_spot["name"],
latitude=source_spot["latitude"],
longitude=source_spot["longitude"],
)
],
time=datetime.strptime(source_spot["spotTime"], "%Y-%m-%dT%H:%M:%S")
.replace(tzinfo=pytz.UTC)
.timestamp(),
dx_grid=source_spot["grid6"],
dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"],
)
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.
+15 -9
View File
@@ -18,7 +18,8 @@ class RBN(SpotProvider):
_LINE_PATTERN = re.compile(
r"^DX de ([a-z0-9/]+)-.*:\s+([0-9.]+)\s+([a-z0-9/]+)\s+(.*)\s+(\d{4}Z)",
re.IGNORECASE)
re.IGNORECASE,
)
def __init__(self, provider_config):
"""Constructor requires port number."""
@@ -64,14 +65,19 @@ class RBN(SpotProvider):
match = self._LINE_PATTERN.match(telnet_output.decode("latin-1"))
if match:
spot_time = datetime.strptime(match.group(5), "%H%MZ")
spot_datetime = datetime.combine(datetime.now(pytz.UTC).date(), spot_time.time(),
tzinfo=pytz.UTC)
spot = Spot(source=self.name,
dx_call=match.group(3),
de_call=match.group(1),
freq=float(match.group(2)) * 1000,
comment=match.group(4).strip(),
time=spot_datetime.timestamp())
spot_datetime = datetime.combine(
datetime.now(pytz.UTC).date(),
spot_time.time(),
tzinfo=pytz.UTC,
)
spot = Spot(
source=self.name,
dx_call=match.group(3),
de_call=match.group(1),
freq=float(match.group(2)) * 1000,
comment=match.group(4).strip(),
time=spot_datetime.timestamp(),
)
# Add to our list
self._submit(spot)
+36 -23
View File
@@ -2,9 +2,9 @@ import logging
from datetime import datetime
import requests
from requests.exceptions import ConnectionError, ReadTimeout, ConnectTimeout
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
from core.constants import HTTP_HEADERS, SSB_SUB_MODES, DV_SUB_MODES
from core.constants import DV_SUB_MODES, HTTP_HEADERS, SSB_SUB_MODES
from data.sig_ref import SIGRef
from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider
@@ -41,24 +41,33 @@ class SOTA(HTTPSpotProvider):
# Iterate through source data
for source_spot in source_data:
# Convert to our spot format
spot = Spot(source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["activatorCallsign"].upper(),
dx_name=source_spot["activatorName"],
de_call=source_spot["callsign"].upper(),
freq=(float(source_spot["frequency"]) * 1000000) if (
source_spot["frequency"] is not None) else None,
# Seen SOTA spots with no frequency!
mode=source_spot["mode"].upper(),
comment=source_spot["comments"],
spot = Spot(
source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["activatorCallsign"].upper(),
dx_name=source_spot["activatorName"],
de_call=source_spot["callsign"].upper(),
freq=(float(source_spot["frequency"]) * 1000000)
if (source_spot["frequency"] is not None)
else None,
# Seen SOTA spots with no frequency!
mode=source_spot["mode"].upper(),
comment=source_spot["comments"],
sig="SOTA",
sig_refs=[
SIGRef(
id=source_spot["summitCode"],
sig="SOTA",
sig_refs=[SIGRef(id=source_spot["summitCode"], sig="SOTA",
name=source_spot["summitName"], latitude=source_spot["latitude"],
longitude=source_spot["longitude"],
activation_score=source_spot["points"])],
dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"],
time=datetime.fromisoformat(source_spot["timeStamp"].replace("Z", "+00:00")).timestamp())
name=source_spot["summitName"],
latitude=source_spot["latitude"],
longitude=source_spot["longitude"],
activation_score=source_spot["points"],
)
],
dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"],
time=datetime.fromisoformat(source_spot["timeStamp"].replace("Z", "+00:00")).timestamp(),
)
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.
@@ -66,7 +75,7 @@ class SOTA(HTTPSpotProvider):
except ConnectionError:
logging.warning("Connection error when accessing SOTA spots API")
except (ConnectTimeout, ReadTimeout):
logging.warning(f"Timeout when accessing SOTA spots API.")
logging.warning("Timeout when accessing SOTA spots API.")
return new_spots
def can_submit_spot(self, sig):
@@ -102,10 +111,14 @@ class SOTA(HTTPSpotProvider):
"mode": mode or "",
"callsign": spot.de_call,
"comments": spot.comment or "",
"type": "TEST" # todo replatce with NORMAL/QRT once testing complete
"type": "TEST", # todo replatce with NORMAL/QRT once testing complete
}
headers = {
**HTTP_HEADERS,
"Authorization": f"bearer {access_token}",
"id_token": id_token,
"Content-Type": "application/json",
}
headers = {**HTTP_HEADERS, "Authorization": f"bearer {access_token}", "id_token": id_token,
"Content-Type": "application/json"}
response = requests.post(self.SUBMIT_URL, json=body, headers=headers, timeout=(5, 30))
if not response.ok:
raise RuntimeError(f"SOTA API returned {response.status_code!s}: {response.text}")
+1 -1
View File
@@ -32,7 +32,7 @@ class SpotProvider:
# Sort the batch so that earliest ones go in first. This helps keep the ordering correct when spots are fired
# off to SSE listeners.
spots = sorted(spots, key=lambda s: (s.time if s and s.time else 0))
spots = sorted(spots, key=lambda s: s.time if s and s.time else 0)
for spot in spots:
if datetime.fromtimestamp(spot.time, pytz.UTC) > self.last_spot_time:
# Fill in any blanks and add to the list
+13 -7
View File
@@ -37,8 +37,7 @@ class SSESpotProvider(SpotProvider):
try:
event_source.close()
except Exception:
logging.exception(
f"Exception closing SSE connection for {self.name} during stop()")
logging.exception(f"Exception closing SSE connection for {self.name} during stop()")
if self._thread:
self._thread.join(timeout=15)
@@ -60,14 +59,20 @@ class SSESpotProvider(SpotProvider):
try:
logging.debug(f"Connecting to {self.name} spot API...")
self.status = "Connecting"
with EventSource(self._url, headers=HTTP_HEADERS, latest_event_id=self._last_event_id, timeout=10,
on_open=self._on_open, on_error=self._on_error) as event_source:
with EventSource(
self._url,
headers=HTTP_HEADERS,
latest_event_id=self._last_event_id,
timeout=10,
on_open=self._on_open,
on_error=self._on_error,
) as event_source:
self._set_event_source(event_source)
try:
for event in event_source:
if self._stop_event.is_set():
break
if event.type == 'message':
if event.type == "message":
try:
self._last_event_id = event.last_event_id
new_spot = self._sse_message_to_spot(event.data)
@@ -80,7 +85,8 @@ class SSESpotProvider(SpotProvider):
except Exception:
logging.exception(
f"Exception processing message from SSE Spot Provider ({self.name})")
f"Exception processing message from SSE Spot Provider ({self.name})"
)
finally:
self._set_event_source(None)
@@ -89,7 +95,7 @@ class SSESpotProvider(SpotProvider):
logging.exception(f"Exception in SSE Spot Provider ({self.name})")
else:
self.status = "Disconnected"
self._stop_event.wait(timeout=5) # Wait before trying to reconnect
self._stop_event.wait(timeout=5) # Wait before trying to reconnect
def _sse_message_to_spot(self, message_data):
"""Convert an SSE message received from the API into a spot. The whole message data is provided here so the subclass
+47 -26
View File
@@ -14,8 +14,22 @@ class Tiles(HTTPSpotProvider):
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://icneuzxitdqtofutxbla.supabase.co/functions/v1/spots?active_hours=24"
SUBMIT_URL = "https://icneuzxitdqtofutxbla.supabase.co/functions/v1/self-spot"
VALID_MODES = ["SSB", "CW", "FT8", "FT4", "FM", "DMR", "D-STAR", "M17", "AX.25", "JS8Call", "PSK31", "Olivia",
"VarAC", "Other"]
VALID_MODES = [
"SSB",
"CW",
"FT8",
"FT4",
"FM",
"DMR",
"D-STAR",
"M17",
"AX.25",
"JS8Call",
"PSK31",
"Olivia",
"VarAC",
"Other",
]
def __init__(self, provider_config):
super().__init__("Tiles", provider_config, self.SPOTS_URL, self.POLL_INTERVAL_SEC)
@@ -25,25 +39,33 @@ class Tiles(HTTPSpotProvider):
# Iterate through source data
for source_spot in http_response.json()["spots"]:
# Convert to our spot format
spot = Spot(source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["call_sign"].upper(),
# No separate spotter callsign, assume all spots are self-spots
de_call=source_spot["call_sign"].upper(),
freq=float(strip_extra_decimal_points(source_spot["frequency"])) * 1000000,
mode=source_spot["mode"].upper(),
comment=source_spot["notes"],
spot = Spot(
source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["call_sign"].upper(),
# No separate spotter callsign, assume all spots are self-spots
de_call=source_spot["call_sign"].upper(),
freq=float(strip_extra_decimal_points(source_spot["frequency"])) * 1000000,
mode=source_spot["mode"].upper(),
comment=source_spot["notes"],
sig="Tiles",
# Tiles spots can include POTA & SOTA references, but ignore those on the basis that we will get them separately from the POTA/SOTA providers anyway.
# Just take the grid reference itself as the single Tiles SIG reference.
sig_refs=[
SIGRef(
id=source_spot["maidenhead_grid"],
sig="Tiles",
# Tiles spots can include POTA & SOTA references, but ignore those on the basis that we will get them separately from the POTA/SOTA providers anyway.
# Just take the grid reference itself as the single Tiles SIG reference.
sig_refs=[SIGRef(id=source_spot["maidenhead_grid"], sig="Tiles",
name=source_spot["maidenhead_grid"], latitude=source_spot["latitude"],
longitude=source_spot["longitude"])],
time=datetime.fromisoformat(source_spot["created_at"].replace("Z", "+00:00")).timestamp(),
dx_grid=source_spot["maidenhead_grid"],
dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"],
dx_location_source="GRID")
name=source_spot["maidenhead_grid"],
latitude=source_spot["latitude"],
longitude=source_spot["longitude"],
)
],
time=datetime.fromisoformat(source_spot["created_at"].replace("Z", "+00:00")).timestamp(),
dx_grid=source_spot["maidenhead_grid"],
dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"],
dx_location_source="GRID",
)
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.
@@ -56,7 +78,6 @@ class Tiles(HTTPSpotProvider):
def submit_spot(self, spot, credentials):
# Tiles on the air currently only supports *self* spots
if spot.dx_call == spot.de_call:
# Figure out a valid mode. Borrowed this from PoLo :)
# https://github.com/ham2k/app-polo/blob/main/src/extensions/activities/sota/SOTAPostSelfSpot.js
if spot.mode:
@@ -80,24 +101,24 @@ class Tiles(HTTPSpotProvider):
"lat": spot.dx_latitude or None,
"lon": spot.dx_longitude or None,
"qrt": spot.qrt or False,
"pin": credentials.get("offline_spot_gateway_pin", "")
"pin": credentials.get("offline_spot_gateway_pin", ""),
}
headers = {**HTTP_HEADERS, "Content-Type": "application/json"}
response = requests.post(self.SUBMIT_URL, json=body, headers=headers, timeout=(5, 30))
if not response.ok:
raise RuntimeError(
f"Tiles on the Air API returned {response.status_code!s}: {response.text}")
raise RuntimeError(f"Tiles on the Air API returned {response.status_code!s}: {response.text}")
else:
raise RuntimeError("The Tiles on the Air API requires a mode to be set.")
else:
raise RuntimeError(
"The Tiles on the Air API only supports self-spots, the DX call and spotter call must match.")
"The Tiles on the Air API only supports self-spots, the DX call and spotter call must match."
)
# Utility function to keep the first decimal point in a given string but remove any others. Used to parse Tiles'
# strange frequency format where we can sometimes have e.g. "14.123.5".
def strip_extra_decimal_points(s):
parts = s.split('.', 1)
parts = s.split(".", 1)
if len(parts) == 1:
return s
return f"{parts[0]}.{parts[1].replace('.', '')}"
+11 -8
View File
@@ -26,14 +26,17 @@ class Towers(HTTPSpotProvider):
likely_freq = float(source_spot["freq"]) * 1000
if likely_freq < 1000000:
likely_freq = likely_freq * 1000
spot = Spot(source=self.name,
dx_call=source_spot["call"].upper(),
freq=likely_freq,
comment=source_spot["comment"],
sig="Towers",
sig_refs=[SIGRef(id=source_spot["ref"], sig="Towers")],
time=datetime.strptime(response_json["updated"][:10] + source_spot["time"],
"%Y-%m-%d%H:%M").timestamp())
spot = Spot(
source=self.name,
dx_call=source_spot["call"].upper(),
freq=likely_freq,
comment=source_spot["comment"],
sig="Towers",
sig_refs=[SIGRef(id=source_spot["ref"], sig="Towers")],
time=datetime.strptime(
response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M"
).timestamp(),
)
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.
+38 -23
View File
@@ -36,42 +36,56 @@ class UKPacketNet(HTTPSpotProvider):
# First build a "full" comment combining some of the extra info
comment = listed_port["comment"] if "comment" in listed_port else ""
comment = f"{comment} {listed_port['mode']}" if "mode" in listed_port else comment
comment = f"{comment} {listed_port['modulation']}" if "modulation" in listed_port else comment
comment = f"{comment} {listed_port['baud']!s} baud" if "baud" in listed_port and listed_port[
"baud"] > 0 else comment
comment = (
f"{comment} {listed_port['modulation']}" if "modulation" in listed_port else comment
)
comment = (
f"{comment} {listed_port['baud']!s} baud"
if "baud" in listed_port and listed_port["baud"] > 0
else comment
)
# Get frequency from the comment if it's not set properly in the data structure. This is
# very hacky but a lot of node comments contain their frequency as the first or second
# word of their comment, but not in the proper data structure field.
freq = listed_port["freq"] if "freq" in listed_port and listed_port[
"freq"] > 0 else None
freq = (
listed_port["freq"] if "freq" in listed_port and listed_port["freq"] > 0 else None
)
if not freq and comment:
possible_freq = comment.split(" ")[0].upper().replace("MHZ", "")
if re.match(r"^[0-9.]+$",
possible_freq) and possible_freq != "1200" and possible_freq != "9600":
if (
re.match(r"^[0-9.]+$", possible_freq)
and possible_freq != "1200"
and possible_freq != "9600"
):
freq = float(possible_freq) * 1000000
if not freq and len(comment.split(" ")) > 1:
possible_freq = comment.split(" ")[1].upper().replace("MHZ", "")
if re.match(r"^[0-9.]+$",
possible_freq) and possible_freq != "1200" and possible_freq != "9600":
if (
re.match(r"^[0-9.]+$", possible_freq)
and possible_freq != "1200"
and possible_freq != "9600"
):
freq = float(possible_freq) * 1000000
# Check for a found frequency likely having been in kHz, sorry to all GHz packet folks
if freq and freq > 1000000000:
freq = freq / 1000
# Now build the spot object
spot = Spot(source=self.name,
dx_call=heard["callsign"].upper(),
de_call=node["callsign"].upper(),
freq=freq,
mode="PKT",
comment=comment,
time=datetime.strptime(heard["lastHeard"], "%Y-%m-%d %H:%M:%S").replace(
tzinfo=pytz.UTC).timestamp(),
de_grid=node["location"]["locator"] if "locator" in node[
"location"] else None,
de_latitude=node["location"]["coords"]["lat"],
de_longitude=node["location"]["coords"]["lon"])
spot = Spot(
source=self.name,
dx_call=heard["callsign"].upper(),
de_call=node["callsign"].upper(),
freq=freq,
mode="PKT",
comment=comment,
time=datetime.strptime(heard["lastHeard"], "%Y-%m-%d %H:%M:%S")
.replace(tzinfo=pytz.UTC)
.timestamp(),
de_grid=node["location"]["locator"] if "locator" in node["location"] else None,
de_latitude=node["location"]["coords"]["lat"],
de_longitude=node["location"]["coords"]["lon"],
)
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.
@@ -84,8 +98,9 @@ class UKPacketNet(HTTPSpotProvider):
# data, and we can use that to look these up.
for spot in new_spots:
if spot.dx_call in nodes:
spot.dx_grid = nodes[spot.dx_call]["location"]["locator"] if "locator" in nodes[spot.dx_call][
"location"] else None
spot.dx_grid = (
nodes[spot.dx_call]["location"]["locator"] if "locator" in nodes[spot.dx_call]["location"] else None
)
spot.dx_latitude = nodes[spot.dx_call]["location"]["coords"]["lat"]
spot.dx_longitude = nodes[spot.dx_call]["location"]["coords"]["lon"]
+1 -2
View File
@@ -60,8 +60,7 @@ class WebsocketSpotProvider(SpotProvider):
logging.debug(f"Received data from {self.name} spot API.")
except Exception:
logging.exception(
f"Exception processing message from Websocket Spot Provider ({self.name})")
logging.exception(f"Exception processing message from Websocket Spot Provider ({self.name})")
except Exception as e:
self.status = "Error"
+18 -13
View File
@@ -28,10 +28,13 @@ class WOTA(HTTPSpotProvider):
rss = cast(RSS, Parser.parse(http_response.content.decode("utf-8-sig")))
# Iterate through source data
for source_spot in rss.channel.items:
try:
# Reject GUID missing or zero
if not source_spot.guid or not source_spot.guid.content or source_spot.guid.content == "http://www.wota.org.uk/spots/0":
if (
not source_spot.guid
or not source_spot.guid.content
or source_spot.guid.content == "http://www.wota.org.uk/spots/0"
):
continue
# Pick apart the title
@@ -48,7 +51,7 @@ class WOTA(HTTPSpotProvider):
# Pick apart the description
desc_split = source_spot.description.split(". ")
freq_mode = desc_split[0].replace("Frequencies/modes:", "").strip()
freq_mode_split = re.split(r'[\-\s]+', freq_mode)
freq_mode_split = re.split(r"[\-\s]+", freq_mode)
freq_hz = float(freq_mode_split[0].replace("'", ".")) * 1000000
mode = None
if len(freq_mode_split) > 1:
@@ -64,16 +67,18 @@ class WOTA(HTTPSpotProvider):
time = datetime.strptime(source_spot.pub_date.content, self.RSS_DATE_TIME_FORMAT).astimezone(pytz.UTC)
# Convert to our spot format
spot = Spot(source=self.name,
source_id=source_spot.guid.content,
dx_call=dx_call,
de_call=spotter,
freq=freq_hz,
mode=mode,
comment=comment,
sig="WOTA",
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
time=time.timestamp())
spot = Spot(
source=self.name,
source_id=source_spot.guid.content,
dx_call=dx_call,
de_call=spotter,
freq=freq_hz,
mode=mode,
comment=comment,
sig="WOTA",
sig_refs=[SIGRef(id=ref, sig="WOTA", name=ref_name)] if ref else [],
time=time.timestamp(),
)
new_spots.append(spot)
except Exception as e:
+24 -17
View File
@@ -20,25 +20,32 @@ class WWBOTA(SSESpotProvider):
# n-fer activations.
refs = []
for ref in source_spot["references"]:
sigref = SIGRef(id=ref["reference"], sig="WWBOTA", name=ref["name"], latitude=ref["lat"],
longitude=ref["long"])
sigref = SIGRef(
id=ref["reference"],
sig="WWBOTA",
name=ref["name"],
latitude=ref["lat"],
longitude=ref["long"],
)
refs.append(sigref)
spot = Spot(source=self.name,
dx_call=source_spot["call"].upper(),
de_call=source_spot["spotter"].upper(),
freq=float(source_spot["freq"]) * 1000000,
mode=source_spot["mode"].upper(),
comment=source_spot["comment"],
sig="WWBOTA",
sig_refs=refs,
time=datetime.fromisoformat(source_spot["time"].replace("Z", "+00:00")).timestamp(),
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For
# now, we will just pick the first one to use as our grid, latitude and longitude.
dx_grid=source_spot["references"][0]["locator"],
dx_latitude=source_spot["references"][0]["lat"],
dx_longitude=source_spot["references"][0]["long"],
qrt=source_spot["type"] == "QRT")
spot = Spot(
source=self.name,
dx_call=source_spot["call"].upper(),
de_call=source_spot["spotter"].upper(),
freq=float(source_spot["freq"]) * 1000000,
mode=source_spot["mode"].upper(),
comment=source_spot["comment"],
sig="WWBOTA",
sig_refs=refs,
time=datetime.fromisoformat(source_spot["time"].replace("Z", "+00:00")).timestamp(),
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For
# now, we will just pick the first one to use as our grid, latitude and longitude.
dx_grid=source_spot["references"][0]["locator"],
dx_latitude=source_spot["references"][0]["lat"],
dx_longitude=source_spot["references"][0]["long"],
qrt=source_spot["type"] == "QRT",
)
# WWBOTA does support a special "Test" spot type, we need to avoid adding that.
return spot if source_spot["type"] != "Test" else None
+21 -12
View File
@@ -21,19 +21,28 @@ class WWFF(HTTPSpotProvider):
# Iterate through source data
for source_spot in http_response.json():
# 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=float(source_spot["frequency_khz"]) * 1000,
mode=source_spot["mode"].upper(),
comment=source_spot["remarks"],
spot = Spot(
source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["activator"].upper(),
de_call=source_spot["spotter"].upper(),
freq=float(source_spot["frequency_khz"]) * 1000,
mode=source_spot["mode"].upper(),
comment=source_spot["remarks"],
sig="WWFF",
sig_refs=[
SIGRef(
id=source_spot["reference"],
sig="WWFF",
sig_refs=[SIGRef(id=source_spot["reference"], sig="WWFF", name=source_spot["reference_name"],
latitude=source_spot["latitude"], longitude=source_spot["longitude"])],
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"])
name=source_spot["reference_name"],
latitude=source_spot["latitude"],
longitude=source_spot["longitude"],
)
],
time=datetime.fromtimestamp(source_spot["spot_time"], tz=pytz.UTC).timestamp(),
dx_latitude=source_spot["latitude"],
dx_longitude=source_spot["longitude"],
)
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us.
+17 -10
View File
@@ -29,14 +29,21 @@ class XOTA(WebsocketSpotProvider):
string = b.decode("utf-8")
source_spot = json.loads(string)
ref_id = f"{self._sig_ref_prefix} {source_spot['reference']['title']}"
spot = Spot(source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["stationCallSign"].upper(),
freq=float(source_spot["freq"]) * 1000,
mode=source_spot["mode"].upper(),
sig=self.SIG,
sig_refs=[
SIGRef(id=ref_id, sig=self.SIG or "", url=source_spot["reference"]["website"])],
time=datetime.now(pytz.UTC).timestamp(),
qrt=source_spot["state"] != "active")
spot = Spot(
source=self.name,
source_id=source_spot["id"],
dx_call=source_spot["stationCallSign"].upper(),
freq=float(source_spot["freq"]) * 1000,
mode=source_spot["mode"].upper(),
sig=self.SIG,
sig_refs=[
SIGRef(
id=ref_id,
sig=self.SIG or "",
url=source_spot["reference"]["website"],
)
],
time=datetime.now(pytz.UTC).timestamp(),
qrt=source_spot["state"] != "active",
)
return spot
+19 -10
View File
@@ -26,17 +26,26 @@ class ZLOTA(HTTPSpotProvider):
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"],
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=[
SIGRef(
id=source_spot["reference"],
sig="ZLOTA",
sig_refs=[SIGRef(id=source_spot["reference"], sig="ZLOTA", name=source_spot["name"])],
time=datetime.fromisoformat(source_spot["referenced_time"].replace("Z", "+00:00")).astimezone(
pytz.UTC).timestamp())
name=source_spot["name"],
)
],
time=datetime.fromisoformat(source_spot["referenced_time"].replace("Z", "+00:00"))
.astimezone(pytz.UTC)
.timestamp(),
)
new_spots.append(spot)
return new_spots