Bulk convert comments above classes/functions/methods into proper docstrings

This commit is contained in:
Ian Renton
2026-02-27 14:21:35 +00:00
parent 068c732796
commit 6b18ec6f88
63 changed files with 540 additions and 349 deletions

View File

@@ -10,8 +10,8 @@ from data.spot import Spot
from spotproviders.spot_provider import SpotProvider
# Spot provider for the APRS-IS.
class APRSIS(SpotProvider):
"""Spot provider for the APRS-IS."""
def __init__(self, provider_config):
super().__init__(provider_config)
@@ -51,11 +51,12 @@ class APRSIS(SpotProvider):
comment=data["comment"] if "comment" in data else None,
dx_latitude=data["latitude"] if "latitude" in data else None,
dx_longitude=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"
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)
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug("Data received from APRS-IS.")
logging.debug("Data received from APRS-IS.")

View File

@@ -12,9 +12,10 @@ from data.spot import Spot
from spotproviders.spot_provider import SpotProvider
# Spot provider for a DX Cluster. Hostname, port, login_prompt, login_callsign and allow_rbn_spots are provided in config.
# See config-example.yml for examples.
class DXCluster(SpotProvider):
"""Spot provider for a DX Cluster. Hostname, port, login_prompt, login_callsign and allow_rbn_spots are provided in config.
See config-example.yml for examples."""
CALLSIGN_PATTERN = "([a-z|0-9|/]+)"
FREQUENCY_PATTERN = "([0-9|.]+)"
LINE_PATTERN_EXCLUDE_RBN = re.compile(
@@ -24,13 +25,15 @@ class DXCluster(SpotProvider):
"^DX de " + CALLSIGN_PATTERN + "-?#?:\\s+" + FREQUENCY_PATTERN + "\\s+" + CALLSIGN_PATTERN + "\\s+(.*)\\s+(\\d{4}Z)",
re.IGNORECASE)
# Constructor requires hostname and port
def __init__(self, provider_config):
"""Constructor requires hostname and port"""
super().__init__(provider_config)
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.telnet = None
@@ -96,4 +99,4 @@ class DXCluster(SpotProvider):
logging.info("DX Cluster " + self.hostname + " shutting down...")
self.status = "Shutting down"
self.status = "Disconnected"
self.status = "Disconnected"

View File

@@ -10,8 +10,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for General Mountain Activity
class GMA(HTTPSpotProvider):
"""Spot provider for General Mountain Activity"""
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://www.cqgma.org/api/spots/25/"
# GMA spots don't contain the details of the programme they are for, we need a separate lookup for that
@@ -36,9 +37,11 @@ class GMA(HTTPSpotProvider):
sig_refs=[SIGRef(id=source_spot["REF"], sig="", name=source_spot["NAME"])],
time=datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M").replace(
tzinfo=pytz.UTC).timestamp(),
dx_latitude=float(source_spot["LAT"]) if (source_spot["LAT"] and source_spot["LAT"] != "") else None,
dx_latitude=float(source_spot["LAT"]) if (
source_spot["LAT"] and source_spot["LAT"] != "") else None,
# Seen GMA spots with no (or empty) lat/lon
dx_longitude=float(source_spot["LON"]) if (source_spot["LON"] and source_spot["LON"] != "") else None)
dx_longitude=float(source_spot["LON"]) if (
source_spot["LON"] and source_spot["LON"] != "") else None)
# GMA doesn't give what programme (SIG) the reference is for until we separately look it up.
if "REF" in source_spot:
@@ -83,5 +86,6 @@ class GMA(HTTPSpotProvider):
# that for us.
new_spots.append(spot)
except:
logging.warn("Exception when looking up " + self.REF_INFO_URL_ROOT + source_spot["REF"] + ", ignoring this spot for now")
logging.warn("Exception when looking up " + self.REF_INFO_URL_ROOT + source_spot[
"REF"] + ", ignoring this spot for now")
return new_spots

View File

@@ -10,8 +10,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for HuMPs Excluding Marilyns Award
class HEMA(HTTPSpotProvider):
"""Spot provider for HuMPs Excluding Marilyns Award"""
POLL_INTERVAL_SEC = 300
# HEMA wants us to check for a "spot seed" from the API and see if it's actually changed before querying the main
# data API. So it's actually the SPOT_SEED_URL that we pass into the constructor and get the superclass to call on a
@@ -54,11 +55,12 @@ class HEMA(HTTPSpotProvider):
comment=spotter_comment_match.group(2),
sig="HEMA",
sig_refs=[SIGRef(id=spot_items[3].upper(), sig="HEMA", name=spot_items[4])],
time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(tzinfo=pytz.UTC).timestamp(),
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)
return new_spots
return new_spots

View File

@@ -9,9 +9,9 @@ from core.constants import HTTP_HEADERS
from spotproviders.spot_provider import SpotProvider
# Generic spot provider class for providers that request data via HTTP(S). Just for convenience to avoid code
# duplication. Subclasses of this query the individual APIs for data.
class HTTPSpotProvider(SpotProvider):
"""Generic spot provider class for providers that request data via HTTP(S). Just for convenience to avoid code
duplication. Subclasses of this query the individual APIs for data."""
def __init__(self, provider_config, url, poll_interval):
super().__init__(provider_config)
@@ -55,8 +55,9 @@ class HTTPSpotProvider(SpotProvider):
logging.exception("Exception in HTTP JSON Spot Provider (" + self.name + ")")
self._stop_event.wait(timeout=1)
# Convert an HTTP response returned by the API into spot data. The whole response is provided here so the subclass
# implementations can check for HTTP status codes if necessary, and handle the response as JSON, XML, text, whatever
# the API actually provides.
def http_response_to_spots(self, http_response):
raise NotImplementedError("Subclasses must implement this method")
"""Convert an HTTP response returned by the API into spot data. The whole response is provided here so the subclass
implementations can check for HTTP status codes if necessary, and handle the response as JSON, XML, text, whatever
the API actually provides."""
raise NotImplementedError("Subclasses must implement this method")

View File

@@ -5,8 +5,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for Lagos y Lagunas On the Air
class LLOTA(HTTPSpotProvider):
"""Spot provider for Lagos y Lagunas On the Air"""
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://llota.app/api/public/spots"
@@ -38,4 +39,4 @@ class LLOTA(HTTPSpotProvider):
# 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)
return new_spots
return new_spots

View File

@@ -9,8 +9,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for Parks n Peaks
class ParksNPeaks(HTTPSpotProvider):
"""Spot provider for Parks n Peaks"""
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://www.parksnpeaks.org/api/ALL"
SIOTA_LIST_URL = "https://www.silosontheair.com/data/silos.csv"
@@ -26,9 +27,10 @@ class ParksNPeaks(HTTPSpotProvider):
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
de_call=source_spot["actSpoter"].upper() if source_spot["actSpoter"] != "" else None,
# typo exists in API
freq=float(source_spot["actFreq"].replace(",", "")) * 1000000 if (
source_spot["actFreq"] != "") else None,
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"],

View File

@@ -7,8 +7,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for Parks on the Air
class POTA(HTTPSpotProvider):
"""Spot provider for Parks on the Air"""
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://api.pota.app/spot/activator"

View File

@@ -12,17 +12,19 @@ from data.spot import Spot
from spotproviders.spot_provider import SpotProvider
# Spot provider for the Reverse Beacon Network. Connects to a single port, if you want both CW/RTTY (port 7000) and FT8
# (port 7001) you need to instantiate two copies of this. The port is provided as an argument to the constructor.
class RBN(SpotProvider):
"""Spot provider for the Reverse Beacon Network. Connects to a single port, if you want both CW/RTTY (port 7000) and FT8
(port 7001) you need to instantiate two copies of this. The port is provided as an argument to the constructor."""
CALLSIGN_PATTERN = "([a-z|0-9|/]+)"
FREQUENCY_PATTERM = "([0-9|.]+)"
LINE_PATTERN = re.compile(
"^DX de " + CALLSIGN_PATTERN + "-.*:\\s+" + FREQUENCY_PATTERM + "\\s+" + CALLSIGN_PATTERN + "\\s+(.*)\\s+(\\d{4}Z)",
re.IGNORECASE)
# Constructor requires port number.
def __init__(self, provider_config):
"""Constructor requires port number."""
super().__init__(provider_config)
self.port = provider_config["port"]
self.telnet = None
@@ -30,7 +32,6 @@ class RBN(SpotProvider):
self.thread.daemon = True
self.run = True
def start(self):
self.thread.start()
@@ -89,4 +90,4 @@ class RBN(SpotProvider):
logging.info("RBN provider (port " + str(self.port) + ") shutting down...")
self.status = "Shutting down"
self.status = "Disconnected"
self.status = "Disconnected"

View File

@@ -8,8 +8,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for Summits on the Air
class SOTA(HTTPSpotProvider):
"""Spot provider for Summits on the Air"""
POLL_INTERVAL_SEC = 120
# SOTA wants us to check for an "epoch" from the API and see if it's actually changed before querying the main data
# APIs. So it's actually the EPOCH_URL that we pass into the constructor and get the superclass to call on a timer.
@@ -41,14 +42,17 @@ class SOTA(HTTPSpotProvider):
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!
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", name=source_spot["summitName"], activation_score=source_spot["points"])],
sig_refs=[SIGRef(id=source_spot["summitCode"], sig="SOTA", name=source_spot["summitName"],
activation_score=source_spot["points"])],
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.
new_spots.append(spot)
return new_spots
return new_spots

View File

@@ -5,11 +5,12 @@ import pytz
from core.config import MAX_SPOT_AGE
# Generic spot provider class. Subclasses of this query the individual APIs for data.
class SpotProvider:
"""Generic spot provider class. Subclasses of this query the individual APIs for data."""
# Constructor
def __init__(self, provider_config):
"""Constructor"""
self.name = provider_config["name"]
self.enabled = provider_config["enabled"]
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
@@ -18,20 +19,23 @@ class SpotProvider:
self.spots = None
self.web_server = None
# Set up the provider, e.g. giving it the spot list to work from
def setup(self, spots, web_server):
"""Set up the provider, e.g. giving it the spot list to work from"""
self.spots = spots
self.web_server = web_server
# Start the provider. This should return immediately after spawning threads to access the remote resources
def start(self):
"""Start the provider. This should return immediately after spawning threads to access the remote resources"""
raise NotImplementedError("Subclasses must implement this method")
# Submit a batch of spots retrieved from the provider. Only spots that are newer than the last spot retrieved
# by this provider will be added to the spot list, to prevent duplications. Spots passing the check will also have
# their infer_missing() method called to complete their data set. This is called by the API-querying
# subclasses on receiving spots.
def submit_batch(self, spots):
"""Submit a batch of spots retrieved from the provider. Only spots that are newer than the last spot retrieved
by this provider will be added to the spot list, to prevent duplications. Spots passing the check will also have
their infer_missing() method called to complete their data set. This is called by the API-querying
subclasses on receiving spots."""
# 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 spot: (spot.time if spot and spot.time else 0))
@@ -42,10 +46,11 @@ class SpotProvider:
self.add_spot(spot)
self.last_spot_time = datetime.fromtimestamp(max(map(lambda s: s.time, spots)), pytz.UTC)
# Submit a single spot retrieved from the provider. This will be added to the list regardless of its age. Spots
# passing the check will also have their infer_missing() method called to complete their data set. This is called by
# the data streaming subclasses, which can be relied upon not to re-provide old spots.
def submit(self, spot):
"""Submit a single spot retrieved from the provider. This will be added to the list regardless of its age. Spots
passing the check will also have their infer_missing() method called to complete their data set. This is called by
the data streaming subclasses, which can be relied upon not to re-provide old spots."""
# Fill in any blanks and add to the list
spot.infer_missing()
self.add_spot(spot)
@@ -54,10 +59,11 @@ class SpotProvider:
def add_spot(self, spot):
if not spot.expired():
self.spots.add(spot.id, spot, expire=MAX_SPOT_AGE)
# Ping the web server in case we have any SSE connections that need to see this immediately
# Ping the web server in case we have any SSE connections that need to see this immediately
if self.web_server:
self.web_server.notify_new_spot(spot)
# Stop any threads and prepare for application shutdown
def stop(self):
raise NotImplementedError("Subclasses must implement this method")
"""Stop any threads and prepare for application shutdown"""
raise NotImplementedError("Subclasses must implement this method")

View File

@@ -10,8 +10,8 @@ from core.constants import HTTP_HEADERS
from spotproviders.spot_provider import SpotProvider
# Spot provider using Server-Sent Events.
class SSESpotProvider(SpotProvider):
"""Spot provider using Server-Sent Events."""
def __init__(self, provider_config, url):
super().__init__(provider_config)
@@ -62,7 +62,8 @@ class SSESpotProvider(SpotProvider):
logging.debug("Received data from " + self.name + " spot API.")
except Exception as e:
logging.exception("Exception processing message from SSE Spot Provider (" + self.name + ")")
logging.exception(
"Exception processing message from SSE Spot Provider (" + self.name + ")")
except Exception as e:
self.status = "Error"
@@ -71,7 +72,8 @@ class SSESpotProvider(SpotProvider):
self.status = "Disconnected"
sleep(5) # Wait before trying to reconnect
# Convert an SSE message received from the API into a spot. The whole message data is provided here so the subclass
# implementations can handle the message as JSON, XML, text, whatever the API actually provides.
def sse_message_to_spot(self, message_data):
raise NotImplementedError("Subclasses must implement this method")
"""Convert an SSE message received from the API into a spot. The whole message data is provided here so the subclass
implementations can handle the message as JSON, XML, text, whatever the API actually provides."""
raise NotImplementedError("Subclasses must implement this method")

View File

@@ -7,8 +7,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for UK Packet Radio network API
class UKPacketNet(HTTPSpotProvider):
"""Spot provider for UK Packet Radio network API"""
POLL_INTERVAL_SEC = 600
SPOTS_URL = "https://nodes.ukpacketradio.network/api/nodedata"
@@ -35,20 +36,26 @@ 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 = (comment + " " + listed_port["mode"]) if "mode" in listed_port else comment
comment = (comment + " " + listed_port["modulation"]) if "modulation" in listed_port else comment
comment = (comment + " " + str(listed_port["baud"]) + " baud") if "baud" in listed_port and listed_port["baud"] > 0 else comment
comment = (comment + " " + listed_port[
"modulation"]) if "modulation" in listed_port else comment
comment = (comment + " " + str(
listed_port["baud"]) + " 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:
@@ -61,8 +68,10 @@ class UKPacketNet(HTTPSpotProvider):
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,
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"])
@@ -77,7 +86,8 @@ 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"]

View File

@@ -10,8 +10,8 @@ from core.constants import HTTP_HEADERS
from spotproviders.spot_provider import SpotProvider
# Spot provider using websockets.
class WebsocketSpotProvider(SpotProvider):
"""Spot provider using websockets."""
def __init__(self, provider_config, url):
super().__init__(provider_config)
@@ -60,7 +60,8 @@ class WebsocketSpotProvider(SpotProvider):
logging.debug("Received data from " + self.name + " spot API.")
except Exception as e:
logging.exception("Exception processing message from Websocket Spot Provider (" + self.name + ")")
logging.exception(
"Exception processing message from Websocket Spot Provider (" + self.name + ")")
except Exception as e:
self.status = "Error"
@@ -69,7 +70,8 @@ class WebsocketSpotProvider(SpotProvider):
self.status = "Disconnected"
sleep(5) # Wait before trying to reconnect
# Convert a WS message received from the API into a spot. The exact message data (in bytes) is provided here so the
# subclass implementations can handle the message as string, JSON, XML, whatever the API actually provides.
def ws_message_to_spot(self, bytes):
raise NotImplementedError("Subclasses must implement this method")
"""Convert a WS message received from the API into a spot. The exact message data (in bytes) is provided here so the
subclass implementations can handle the message as string, JSON, XML, whatever the API actually provides."""
raise NotImplementedError("Subclasses must implement this method")

View File

@@ -10,8 +10,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for Wainwrights on the Air
class WOTA(HTTPSpotProvider):
"""Spot provider for Wainwrights on the Air"""
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://www.wota.org.uk/spots_rss.php"
LIST_URL = "https://www.wota.org.uk/mapping/data/summits.json"

View File

@@ -6,8 +6,9 @@ from data.spot import Spot
from spotproviders.sse_spot_provider import SSESpotProvider
# Spot provider for Worldwide Bunkers on the Air
class WWBOTA(SSESpotProvider):
"""Spot provider for Worldwide Bunkers on the Air"""
SPOTS_URL = "https://api.wwbota.net/spots/"
def __init__(self, provider_config):

View File

@@ -7,8 +7,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for Worldwide Flora & Fauna
class WWFF(HTTPSpotProvider):
"""Spot provider for Worldwide Flora & Fauna"""
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://spots.wwff.co/static/spots.json"
@@ -36,4 +37,4 @@ class WWFF(HTTPSpotProvider):
# 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)
return new_spots
return new_spots

View File

@@ -1,15 +1,15 @@
from datetime import datetime
import json
import pytz
from data.sig_ref import SIGRef
from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for Towers on the Air
class WWTOTA(HTTPSpotProvider):
"""Spot provider for Towers on the Air"""
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://wwtota.com/api/cluster_live.php"
@@ -33,9 +33,10 @@ class WWTOTA(HTTPSpotProvider):
comment=source_spot["comment"],
sig="WWTOTA",
sig_refs=[SIGRef(id=source_spot["ref"], sig="WWTOTA")],
time=datetime.strptime(response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M").timestamp())
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.
new_spots.append(spot)
return new_spots
return new_spots

View File

@@ -10,12 +10,13 @@ from data.spot import Spot
from spotproviders.websocket_spot_provider import WebsocketSpotProvider
# Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/
# The provider typically doesn't give us a lat/lon or SIG explicitly, so our own config provides a SIG and a reference
# to a local CSV file with location information. This functionality is implemented for TOTA events, of which there are
# several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA and hence which server
# supplied the data, which is why the CSV location lookup is here and not in sig_utils.
class XOTA(WebsocketSpotProvider):
"""Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/
The provider typically doesn't give us a lat/lon or SIG explicitly, so our own config provides a SIG and a reference
to a local CSV file with location information. This functionality is implemented for TOTA events, of which there are
several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA and hence which server
supplied the data, which is why the CSV location lookup is here and not in sig_utils."""
LOCATION_DATA = {}
SIG = None
@@ -47,7 +48,8 @@ class XOTA(WebsocketSpotProvider):
freq=float(source_spot["freq"]) * 1000,
mode=source_spot["mode"].upper(),
sig=self.SIG,
sig_refs=[SIGRef(id=ref_id, sig=self.SIG, url=source_spot["reference"]["website"], latitude=lat, longitude=lon)],
sig_refs=[SIGRef(id=ref_id, sig=self.SIG, url=source_spot["reference"]["website"], latitude=lat,
longitude=lon)],
time=datetime.now(pytz.UTC).timestamp(),
dx_latitude=lat,
dx_longitude=lon,

View File

@@ -7,8 +7,9 @@ from data.spot import Spot
from spotproviders.http_spot_provider import HTTPSpotProvider
# Spot provider for ZLOTA
class ZLOTA(HTTPSpotProvider):
"""Spot provider for ZLOTA"""
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://ontheair.nz/api/spots?zlota_only=true"
LIST_URL = "https://ontheair.nz/assets/assets.json"
@@ -35,7 +36,8 @@ class ZLOTA(HTTPSpotProvider):
comment=source_spot["comments"],
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())
time=datetime.fromisoformat(source_spot["referenced_time"].replace("Z", "+00:00")).astimezone(
pytz.UTC).timestamp())
new_spots.append(spot)
return new_spots