mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-08-05 18:11:41 +00:00
Refactor of caching & data storage part 4 #118
This commit is contained in:
@@ -20,7 +20,7 @@ class BOTA(HTTPAlertProvider):
|
|||||||
def _http_response_to_alerts(self, http_response):
|
def _http_response_to_alerts(self, http_response):
|
||||||
new_alerts = []
|
new_alerts = []
|
||||||
# Find the table of upcoming alerts
|
# Find the table of upcoming alerts
|
||||||
bs = BeautifulSoup(http_response.content.decode(), features="lxml")
|
bs = BeautifulSoup(http_response.content.decode("utf-8-sig"), features="lxml")
|
||||||
if not bs.body:
|
if not bs.body:
|
||||||
return new_alerts
|
return new_alerts
|
||||||
div = bs.body.find('div', attrs={'class': 'view-activations-public'})
|
div = bs.body.find('div', attrs={'class': 'view-activations-public'})
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class NG3K(HTTPAlertProvider):
|
|||||||
|
|
||||||
def _http_response_to_alerts(self, http_response):
|
def _http_response_to_alerts(self, http_response):
|
||||||
new_alerts = []
|
new_alerts = []
|
||||||
rss = cast(RSS, Parser.parse(http_response.content.decode()))
|
rss = cast(RSS, Parser.parse(http_response.content.decode("utf-8-sig")))
|
||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_alert in rss.channel.items:
|
for source_alert in rss.channel.items:
|
||||||
# Deal with "the format"...
|
# Deal with "the format"...
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ParksNPeaks(HTTPAlertProvider):
|
|||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_alert in http_response.json():
|
for source_alert in http_response.json():
|
||||||
# Calculate some things
|
# Calculate some things
|
||||||
sig = source_alert["Class"]
|
sig = source_alert["Class"].upper()
|
||||||
if " - " in source_alert["Location"]:
|
if " - " in source_alert["Location"]:
|
||||||
split = source_alert["Location"].split(" - ")
|
split = source_alert["Location"].split(" - ")
|
||||||
sig_ref = split[0]
|
sig_ref = split[0]
|
||||||
@@ -50,7 +50,7 @@ class ParksNPeaks(HTTPAlertProvider):
|
|||||||
is_dxpedition=False)
|
is_dxpedition=False)
|
||||||
|
|
||||||
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
|
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
|
||||||
if sig and sig not in ["POTA", "SOTA", "WWFF", "SiOTA", "ZLOTA", "KRMNPA", "LLOTA", "QRP"]:
|
if sig and sig not in ["POTA", "SOTA", "WWFF", "SIOTA", "ZLOTA", "KRMNPA", "LLOTA", "QRP"]:
|
||||||
logging.warning("PNP alert found with sig " + sig + ", developer needs to add support for this!")
|
logging.warning("PNP alert found with sig " + sig + ", developer needs to add support for this!")
|
||||||
|
|
||||||
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to
|
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class WOTA(HTTPAlertProvider):
|
|||||||
|
|
||||||
def _http_response_to_alerts(self, http_response):
|
def _http_response_to_alerts(self, http_response):
|
||||||
new_alerts = []
|
new_alerts = []
|
||||||
rss = cast(RSS, RSSParser.parse(http_response.content.decode()))
|
rss = cast(RSS, RSSParser.parse(http_response.content.decode("utf-8-sig")))
|
||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_alert in rss.channel.items:
|
for source_alert in rss.channel.items:
|
||||||
|
|
||||||
|
|||||||
+34
-4
@@ -121,7 +121,7 @@ spot-providers:
|
|||||||
url: "wss://39c3.totawatch.de/api/spot/live"
|
url: "wss://39c3.totawatch.de/api/spot/live"
|
||||||
# For the "XOTA" provider, a SIG must be set menually here because xOTA is a generic backend for xOTA
|
# For the "XOTA" provider, a SIG must be set menually here because xOTA is a generic backend for xOTA
|
||||||
# programmes and so different URLs potentially provide different programmes.
|
# programmes and so different URLs potentially provide different programmes.
|
||||||
sig: "TOTA"
|
sig: "Toilets"
|
||||||
# For Toilets on the Air, we prefix the SIG references (T-01 etc) with some characters that define the conference:
|
# For Toilets on the Air, we prefix the SIG references (T-01 etc) with some characters that define the conference:
|
||||||
# C3, EH or HOPE - so we can look up the correct locations in our database, because each conference starts from T-01
|
# C3, EH or HOPE - so we can look up the correct locations in our database, because each conference starts from T-01
|
||||||
# but refers to a toilet in a different building (or continent!)
|
# but refers to a toilet in a different building (or continent!)
|
||||||
@@ -131,14 +131,14 @@ spot-providers:
|
|||||||
name: "EH23 TOTA"
|
name: "EH23 TOTA"
|
||||||
enabled: false
|
enabled: false
|
||||||
url: "wss://eh23.totawatch.de/api/spot/live"
|
url: "wss://eh23.totawatch.de/api/spot/live"
|
||||||
sig: "TOTA"
|
sig: "Toilets"
|
||||||
sig-ref-prefix: "EH"
|
sig-ref-prefix: "EH"
|
||||||
|
|
||||||
- class: "XOTA"
|
- class: "XOTA"
|
||||||
name: "HOPE26 TOTA"
|
name: "HOPE26 TOTA"
|
||||||
enabled: false
|
enabled: false
|
||||||
url: "wss://hope-26.totawatch.de/api/spot/live"
|
url: "wss://hope-26.totawatch.de/api/spot/live"
|
||||||
sig: "TOTA"
|
sig: "Toilets"
|
||||||
sig-ref-prefix: "HOPE"
|
sig-ref-prefix: "HOPE"
|
||||||
|
|
||||||
|
|
||||||
@@ -169,9 +169,36 @@ alert-providers:
|
|||||||
# SIG reference data providers to use. This allows Spothole to download, for example, the WWFF directory that maps WWFF
|
# SIG reference data providers to use. This allows Spothole to download, for example, the WWFF directory that maps WWFF
|
||||||
# park IDs to their name and location.
|
# park IDs to their name and location.
|
||||||
sig-ref-data-providers:
|
sig-ref-data-providers:
|
||||||
|
- class: "POTA"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- class: "SOTA"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
- class: "WWFF"
|
- class: "WWFF"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
- class: "WWBOTA"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- class: "GMA"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- class: "MOTA"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- class: "ILLW"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- class: "ARLHS"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- class: "WCA"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- class: "IOTA"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
- class: "WOTA"
|
- class: "WOTA"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
@@ -184,10 +211,13 @@ sig-ref-data-providers:
|
|||||||
- class: "LLOTA"
|
- class: "LLOTA"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
- class: "Towers"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
- class: "DME"
|
- class: "DME"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
- class: "TOTA"
|
- class: "Toilets"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# Solar condition providers to use. These poll external APIs for solar propagation data (SFI, A/K indices, band
|
# Solar condition providers to use. These poll external APIs for solar propagation data (SFI, A/K indices, band
|
||||||
|
|||||||
+2
-2
@@ -19,9 +19,9 @@ SIGS = [
|
|||||||
SIG(name="HEMA", comment_names=["HEMA"], description="HuMPs Excluding Marilyns Award", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}"),
|
SIG(name="HEMA", comment_names=["HEMA"], description="HuMPs Excluding Marilyns Award", ref_regex=r"[A-Z0-9]{1,3}\/[A-Z]{3}\-\d{3}"),
|
||||||
SIG(name="IOTA", comment_names=["IOTA"], description="Islands on the Air", ref_regex=r"[A-Z]{2}\-\d{3}"),
|
SIG(name="IOTA", comment_names=["IOTA"], description="Islands on the Air", ref_regex=r"[A-Z]{2}\-\d{3}"),
|
||||||
SIG(name="MOTA", comment_names=["MOTA"], description="Mills on the Air", ref_regex=r"X\d{4,6}"),
|
SIG(name="MOTA", comment_names=["MOTA"], description="Mills on the Air", ref_regex=r"X\d{4,6}"),
|
||||||
SIG(name="ARLHS", comment_names=["ARLHS"], description="Amateur Radio Lighthouse Society", ref_regex=r"[A-Z]{3}\-\d{3,4}"),
|
SIG(name="ARLHS", comment_names=["ARLHS"], description="Amateur Radio Lighthouse Society", ref_regex=r"[A-Z]{3}[\- ]\d{3,4}"),
|
||||||
SIG(name="ILLW", comment_names=["ILLW"], description="International Lighthouse & Lightship Weekend", ref_regex=r"[A-Z]{2}\d{4}"),
|
SIG(name="ILLW", comment_names=["ILLW"], description="International Lighthouse & Lightship Weekend", ref_regex=r"[A-Z]{2}\d{4}"),
|
||||||
SIG(name="SiOTA", comment_names=["SIOTA"], description="Silos on the Air", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"),
|
SIG(name="SIOTA", comment_names=["SIOTA"], description="Silos on the Air", ref_regex=r"[A-Z]{2}\-[A-Z]{3}\d"),
|
||||||
SIG(name="WCA", comment_names=["WCA"], description="World Castles Award", ref_regex=r"[A-Z0-9]{1,3}\-\d{5}"),
|
SIG(name="WCA", comment_names=["WCA"], description="World Castles Award", ref_regex=r"[A-Z0-9]{1,3}\-\d{5}"),
|
||||||
SIG(name="ZLOTA", comment_names=["ZLOTA"], description="New Zealand on the Air", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}"),
|
SIG(name="ZLOTA", comment_names=["ZLOTA"], description="New Zealand on the Air", ref_regex=r"ZL[A-Z]/[A-Z]{2}\-\d{3,4}"),
|
||||||
SIG(name="WOTA", comment_names=["WOTA"], description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
|
SIG(name="WOTA", comment_names=["WOTA"], description="Wainwrights on the Air", ref_regex=r"[A-Z]{3}-[0-9]{2}"),
|
||||||
|
|||||||
+40
-24
@@ -1,8 +1,10 @@
|
|||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import diskcache
|
import diskcache
|
||||||
|
|
||||||
from core.config import MAX_SPOT_AGE, MAX_ALERT_AGE
|
from core.config import MAX_SPOT_AGE, MAX_ALERT_AGE
|
||||||
|
from core.constants import SIGS
|
||||||
from core.live_data_cache import LiveDataCache
|
from core.live_data_cache import LiveDataCache
|
||||||
from data.solar_conditions import SolarConditions
|
from data.solar_conditions import SolarConditions
|
||||||
|
|
||||||
@@ -12,51 +14,65 @@ class DataStore:
|
|||||||
lookup data using different caching strategies for each."""
|
lookup data using different caching strategies for each."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.CACHE_DIR = "./cache"
|
self._CACHE_DIR = "./cache"
|
||||||
self.MAX_SPOT_COUNT = 10000
|
self._MAX_SPOT_COUNT = 100000
|
||||||
self.MAX_ALERT_COUNT = 10000
|
self._MAX_ALERT_COUNT = 100000
|
||||||
self.SPOT_ALERT_SNAPSHOT_INTERVAL_SEC = 300
|
self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC = 300
|
||||||
self.CALLSIGN_DATA_TTL_SEC = 30 * 24 * 60 * 60
|
self._CALLSIGN_DATA_TTL_SEC = 30 * 24 * 60 * 60
|
||||||
|
self.alerts = None
|
||||||
|
self.spots = None
|
||||||
|
self.callsigns = None
|
||||||
|
self.sigrefs = None
|
||||||
|
self.status_data = None
|
||||||
|
self._status = None
|
||||||
|
self.solar_conditions = None
|
||||||
|
self._solar = None
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
Path(self.CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
Path(self._CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Standard disk cache for solar data and status data, but each cache contains only a single object which we
|
# Standard disk cache for solar data and status data, but each cache contains only a single object which we
|
||||||
# expose to the wider application
|
# expose to the wider application
|
||||||
self.solar = diskcache.Cache(self.CACHE_DIR + "/solar")
|
self._solar = diskcache.Cache(self._CACHE_DIR + "/solar")
|
||||||
if "solar_conditions" not in self.solar:
|
if "solar_conditions" not in self._solar:
|
||||||
self.solar.add("solar_conditions", SolarConditions())
|
self._solar.add("solar_conditions", SolarConditions())
|
||||||
self.solar_conditions = self.solar.get("solar_conditions")
|
self.solar_conditions = self._solar.get("solar_conditions")
|
||||||
self.status = diskcache.Cache(self.CACHE_DIR + "/status")
|
self._status = diskcache.Cache(self._CACHE_DIR + "/status")
|
||||||
if "status_data" not in self.status:
|
if "status_data" not in self._status:
|
||||||
self.status.add("status_data", {})
|
self._status.add("status_data", {})
|
||||||
self.status_data = self.status.get("status_data")
|
self.status_data = self._status.get("status_data")
|
||||||
|
|
||||||
# Standard disk cache for SIG ref data. Separate provider threads will repopulate theis on a regular basis
|
# Standard disk cache for SIG ref data. Separate provider threads will repopulate theis on a regular basis
|
||||||
# but there's no need for a TTL since old data is better than no data. This is a two-layer dict, keys are SIG
|
# but there's no need for a TTL since old data is better than no data. This is a two-layer dict, keys are SIG
|
||||||
# name and then reference ID, with the final value being a SIGRef object.
|
# name and then reference ID, with the final value being a SIGRef object.
|
||||||
self.sigrefs = diskcache.Cache(self.CACHE_DIR + "/sigrefs")
|
self.sigrefs = diskcache.Cache(self._CACHE_DIR + "/sigrefs")
|
||||||
|
for k in list(self.sigrefs.iterkeys()):
|
||||||
|
logging.info(f"Loaded data for %d references in %s SIG.", len(self.sigrefs[k]), k)
|
||||||
|
|
||||||
# Standard disk cache for callsign data. This data does have a TTL to trigger an occasional re-lookup.
|
# Standard disk cache for callsign data. This data does have a TTL to trigger an occasional re-lookup.
|
||||||
# Old data *is* better than no data, but we can't have a background thread re-looking-up every callsign
|
# Old data *is* better than no data, but we can't have a background thread re-looking-up every callsign
|
||||||
# we've seen, so we rely on them timing out and this triggering another lookup.
|
# we've seen, so we rely on them timing out and this triggering another lookup.
|
||||||
self.callsigns = diskcache.Cache(self.CACHE_DIR + "/callsigns")
|
self.callsigns = diskcache.Cache(self._CACHE_DIR + "/callsigns")
|
||||||
|
logging.info(f"Loaded data for %d callsigns.", len(self.callsigns))
|
||||||
|
|
||||||
# Special caches for spots and alerts, which have TTL and write snapshots to disk at an interval. We
|
# Special caches for spots and alerts, which have TTL and write snapshots to disk at an interval. We
|
||||||
# specifically load these caches *last* so that any sigref and callsign data is already loaded from disk cache
|
# specifically load these caches *last* so that any sigref and callsign data is already loaded from disk cache
|
||||||
# before the spots and alerts are live in the system.
|
# before the spots and alerts are live in the system.
|
||||||
self.spots = LiveDataCache(maxsize=self.MAX_SPOT_COUNT, ttl=MAX_SPOT_AGE,
|
self.spots = LiveDataCache(maxsize=self._MAX_SPOT_COUNT, ttl=MAX_SPOT_AGE,
|
||||||
snapshot_dir=self.CACHE_DIR + "/spots",
|
snapshot_dir=self._CACHE_DIR + "/spots",
|
||||||
snapshot_interval_sec=self.SPOT_ALERT_SNAPSHOT_INTERVAL_SEC)
|
snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC)
|
||||||
self.alerts = LiveDataCache(maxsize=self.MAX_ALERT_COUNT, ttl=MAX_ALERT_AGE,
|
logging.info(f"Loaded %d spots from a previous run.", len(self.spots.keys()))
|
||||||
snapshot_dir=self.CACHE_DIR + "/alerts",
|
|
||||||
snapshot_interval_sec=self.SPOT_ALERT_SNAPSHOT_INTERVAL_SEC)
|
self.alerts = LiveDataCache(maxsize=self._MAX_ALERT_COUNT, ttl=MAX_ALERT_AGE,
|
||||||
|
snapshot_dir=self._CACHE_DIR + "/alerts",
|
||||||
|
snapshot_interval_sec=self._SPOT_ALERT_SNAPSHOT_INTERVAL_SEC)
|
||||||
|
logging.info(f"Loaded %d alerts from a previous run.", len(self.alerts.keys()))
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.spots.close()
|
self.spots.close()
|
||||||
self.alerts.close()
|
self.alerts.close()
|
||||||
self.solar.close()
|
self._solar.close()
|
||||||
self.status.close()
|
self._status.close()
|
||||||
self.sigrefs.close()
|
self.sigrefs.close()
|
||||||
self.callsigns.close()
|
self.callsigns.close()
|
||||||
|
|
||||||
|
|||||||
+54
-142
@@ -35,65 +35,67 @@ def populate_sig_ref_info(sig_ref):
|
|||||||
SIG we are getting data for.
|
SIG we are getting data for.
|
||||||
Note there is currently no support for KRMNPA location lookup, see issue #61."""
|
Note there is currently no support for KRMNPA location lookup, see issue #61."""
|
||||||
|
|
||||||
if sig_ref.sig is None or sig_ref.id is None or sig_ref.id == "":
|
if sig_ref.sig is None or sig_ref.sig == "" or sig_ref.id is None or sig_ref.id == "":
|
||||||
logging.debug("Failed to look up sig_ref info, sig or id were not set.")
|
logging.debug("Failed to look up sig_ref info, sig or id were not set.")
|
||||||
return sig_ref
|
return sig_ref
|
||||||
|
|
||||||
sig = sig_ref.sig or ""
|
sig = sig_ref.sig
|
||||||
ref_id = sig_ref.id
|
ref_id = sig_ref.id
|
||||||
|
|
||||||
|
# DME fudge. Our database has leading zeros padding to 5 digits which is the expected format, but not all activators
|
||||||
|
# add leading zeros.
|
||||||
|
if sig.upper() == "DME":
|
||||||
|
ref_id = ref_id.zfill(5)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if sig.upper() == "POTA":
|
# If the SIG is HEMA or KRMNPA, we have no current lookup for this so just skip it.
|
||||||
response = URL_DATA_CACHE.get("https://api.pota.app/park/" + ref_id, headers=HTTP_HEADERS)
|
if sig.upper() == "HEMA" or sig.upper() == "KRMNPA":
|
||||||
if response.ok:
|
return sig_ref
|
||||||
data = response.json()
|
|
||||||
if data:
|
|
||||||
fullname = str(data["name"]) if "name" in data else None
|
|
||||||
if fullname and "parktypeDesc" in data and data["parktypeDesc"] != "":
|
|
||||||
fullname = fullname + " " + data["parktypeDesc"]
|
|
||||||
sig_ref.name = fullname
|
|
||||||
sig_ref.url = "https://pota.app/#/park/" + ref_id
|
|
||||||
sig_ref.grid = data["grid6"] if "grid6" in data else None
|
|
||||||
sig_ref.latitude = data["latitude"] if "latitude" in data else None
|
|
||||||
sig_ref.longitude = data["longitude"] if "longitude" in data else None
|
|
||||||
elif not response.from_cache:
|
|
||||||
logging.warning("Malformed response looking up %s ref %s", sig, ref_id)
|
|
||||||
elif not response.from_cache:
|
|
||||||
logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
|
|
||||||
|
|
||||||
elif sig.upper() == "SOTA":
|
# If the SIG is Tiles, WAB, WAI or BOTA (Beaches), we don't have anything to look up from the data store, we can
|
||||||
response = URL_DATA_CACHE.get("https://api-db2.sota.org.uk/api/summits/" + ref_id,
|
# calculate all the information we are going to get directly. So handle those cases first
|
||||||
headers=HTTP_HEADERS)
|
elif sig.upper() == "TILES":
|
||||||
if response.ok:
|
# Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same
|
||||||
data = response.json()
|
if not sig_ref.name:
|
||||||
if data:
|
sig_ref.name = sig_ref.id
|
||||||
sig_ref.name = data["name"] if "name" in data else None
|
if not sig_ref.grid:
|
||||||
sig_ref.url = "https://www.sotadata.org.uk/en/summit/" + ref_id
|
sig_ref.grid = sig_ref.id
|
||||||
sig_ref.grid = data["locator"] if "locator" in data else None
|
if sig_ref.grid and not sig_ref.latitude:
|
||||||
sig_ref.latitude = data["latitude"] if "latitude" in data else None
|
ll = locator_to_latlong(str(sig_ref.grid))
|
||||||
sig_ref.longitude = data["longitude"] if "longitude" in data else None
|
sig_ref.latitude = ll[0]
|
||||||
sig_ref.activation_score = data["points"] if "points" in data else None
|
sig_ref.longitude = ll[1]
|
||||||
elif not response.from_cache:
|
|
||||||
logging.warning("Malformed response looking up %s ref %s", sig, ref_id)
|
|
||||||
elif not response.from_cache:
|
|
||||||
logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
|
|
||||||
|
|
||||||
elif sig.upper() == "WWBOTA":
|
elif sig.upper() == "WAB" or sig.upper() == "WAI":
|
||||||
response = URL_DATA_CACHE.get("https://api.wwbota.org/bunkers/" + ref_id,
|
ll = wab_wai_square_to_lat_lon(ref_id)
|
||||||
headers=HTTP_HEADERS)
|
if ll:
|
||||||
if response.ok:
|
sig_ref.name = ref_id
|
||||||
data = response.json()
|
try:
|
||||||
if data:
|
sig_ref.grid = latlong_to_locator(ll[0], ll[1], 6)
|
||||||
sig_ref.name = data["name"] if "name" in data else None
|
sig_ref.latitude = ll[0]
|
||||||
sig_ref.url = "https://bunkerwiki.org/?s=" + ref_id if ref_id.startswith("B/G") else None
|
sig_ref.longitude = ll[1]
|
||||||
sig_ref.grid = data["locator"] if "locator" in data else None
|
except:
|
||||||
sig_ref.latitude = data["lat"] if "lat" in data else None
|
logging.warning("Invalid lat/lon received for WAB/WAI reference")
|
||||||
sig_ref.longitude = data["long"] if "long" in data else None
|
|
||||||
elif not response.from_cache:
|
|
||||||
logging.warning("Malformed response looking up %s ref %s", sig, ref_id)
|
|
||||||
elif not response.from_cache:
|
|
||||||
logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
|
|
||||||
|
|
||||||
elif sig.upper() == "GMA" or sig.upper() == "ARLHS" or sig.upper() == "ILLW" or sig.upper() == "WCA" or sig.upper() == "MOTA" or sig.upper() == "IOTA":
|
elif sig.upper() == "BOTA":
|
||||||
|
# For BOTA all we can ever generate is the URL, there is no data file or lookup for lat/longs
|
||||||
|
if not sig_ref.name:
|
||||||
|
sig_ref.name = sig_ref.id
|
||||||
|
sig_ref.url = "https://www.beachesontheair.com/beaches/" + sig_ref.name.lower().replace(" ", "-")
|
||||||
|
|
||||||
|
# OK, this is something we have to look up. Now check to see if our data store contains SIG ref information for
|
||||||
|
# this SIG. If so, check for the reference data and use that.
|
||||||
|
elif sig in DATA_STORE.sigrefs:
|
||||||
|
lookup_data = DATA_STORE.sigrefs[sig][ref_id] if ref_id in DATA_STORE.sigrefs[sig] else None
|
||||||
|
if lookup_data:
|
||||||
|
# Copy new sig ref data into existing object
|
||||||
|
sig_ref.__dict__.update(lookup_data.__dict__)
|
||||||
|
else:
|
||||||
|
logging.warning("%s database did not contain data for ref %s", sig, ref_id)
|
||||||
|
|
||||||
|
elif False:
|
||||||
|
# TODO remove
|
||||||
|
# OK, this is not a SIG we have stored data for. Maybe it's of a type we can query information for live.
|
||||||
|
if sig.upper() == "IOTA":
|
||||||
response = URL_DATA_CACHE.get("https://www.cqgma.org/api/ref/?" + ref_id,
|
response = URL_DATA_CACHE.get("https://www.cqgma.org/api/ref/?" + ref_id,
|
||||||
headers=HTTP_HEADERS)
|
headers=HTTP_HEADERS)
|
||||||
if response.ok:
|
if response.ok:
|
||||||
@@ -119,99 +121,9 @@ def populate_sig_ref_info(sig_ref):
|
|||||||
elif not response.from_cache:
|
elif not response.from_cache:
|
||||||
logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
|
logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
|
||||||
|
|
||||||
elif sig.upper() == "WWFF":
|
|
||||||
lookup_data = DATA_STORE.sigrefs["WWFF"][ref_id] if ref_id in DATA_STORE.sigrefs["WWFF"] else None
|
|
||||||
if lookup_data:
|
|
||||||
# Copy new sig ref data into existing object
|
|
||||||
sig_ref.__dict__.update(lookup_data.__dict__)
|
|
||||||
else:
|
else:
|
||||||
logging.warning("WWFF database did not contain data for ref %s", ref_id)
|
logging.warning(f"Tried to look up a SIG called %s but Spothole does not know what that is.", sig)
|
||||||
|
|
||||||
elif sig.upper() == "SIOTA":
|
|
||||||
lookup_data = DATA_STORE.sigrefs["SIOTA"][ref_id] if ref_id in DATA_STORE.sigrefs["SIOTA"] else None
|
|
||||||
if lookup_data:
|
|
||||||
# Copy new sig ref data into existing object
|
|
||||||
sig_ref.__dict__.update(lookup_data.__dict__)
|
|
||||||
else:
|
|
||||||
logging.warning("SIOTA database did not contain data for ref %s", ref_id)
|
|
||||||
|
|
||||||
elif sig.upper() == "WOTA":
|
|
||||||
lookup_data = DATA_STORE.sigrefs["WOTA"][ref_id] if ref_id in DATA_STORE.sigrefs["WOTA"] else None
|
|
||||||
if lookup_data:
|
|
||||||
# Copy new sig ref data into existing object
|
|
||||||
sig_ref.__dict__.update(lookup_data.__dict__)
|
|
||||||
else:
|
|
||||||
logging.warning("WOTA database did not contain data for ref %s", ref_id)
|
|
||||||
|
|
||||||
elif sig.upper() == "ZLOTA":
|
|
||||||
lookup_data = DATA_STORE.sigrefs["ZLOTA"][ref_id] if ref_id in DATA_STORE.sigrefs["ZLOTA"] else None
|
|
||||||
if lookup_data:
|
|
||||||
# Copy new sig ref data into existing object
|
|
||||||
sig_ref.__dict__.update(lookup_data.__dict__)
|
|
||||||
else:
|
|
||||||
logging.warning("ZLOTA database did not contain data for ref %s", ref_id)
|
|
||||||
|
|
||||||
elif sig.upper() == "BOTA":
|
|
||||||
if not sig_ref.name:
|
|
||||||
sig_ref.name = sig_ref.id
|
|
||||||
sig_ref.url = "https://www.beachesontheair.com/beaches/" + sig_ref.name.lower().replace(" ", "-")
|
|
||||||
|
|
||||||
elif sig.upper() == "LLOTA":
|
|
||||||
lookup_data = DATA_STORE.sigrefs["LLOTA"][ref_id] if ref_id in DATA_STORE.sigrefs["LLOTA"] else None
|
|
||||||
if lookup_data:
|
|
||||||
# Copy new sig ref data into existing object
|
|
||||||
sig_ref.__dict__.update(lookup_data.__dict__)
|
|
||||||
else:
|
|
||||||
logging.warning("LLOTA database did not contain data for ref %s", ref_id)
|
|
||||||
|
|
||||||
elif sig.upper() == "DME":
|
|
||||||
# Zero-pad to 5 digits to match our source data
|
|
||||||
lookup_data = DATA_STORE.sigrefs["DME"][ref_id.zfill(5)] if ref_id.zfill(5) in DATA_STORE.sigrefs["DME"] else None
|
|
||||||
if lookup_data:
|
|
||||||
# Copy new sig ref data into existing object
|
|
||||||
sig_ref.__dict__.update(lookup_data.__dict__)
|
|
||||||
else:
|
|
||||||
logging.warning("DME database did not contain data for ref %s", ref_id)
|
|
||||||
|
|
||||||
elif sig.upper() == "TOTA":
|
|
||||||
lookup_data = DATA_STORE.sigrefs["TOTA"][ref_id] if ref_id in DATA_STORE.sigrefs["TOTA"] else None
|
|
||||||
if lookup_data:
|
|
||||||
# Copy new sig ref data into existing object
|
|
||||||
sig_ref.__dict__.update(lookup_data.__dict__)
|
|
||||||
else:
|
|
||||||
logging.warning("TOTA database did not contain data for ref %s", ref_id)
|
|
||||||
|
|
||||||
elif sig.upper() == "WWTOTA":
|
|
||||||
if not sig_ref.name:
|
|
||||||
sig_ref.name = sig_ref.id
|
|
||||||
sig_ref.url = "https://wwtota.com/seznam/karta_rozhledny.php?ref=" + str(sig_ref.name)
|
|
||||||
|
|
||||||
elif sig.upper() == "TILES":
|
|
||||||
# Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same
|
|
||||||
if not sig_ref.name:
|
|
||||||
sig_ref.name = sig_ref.id
|
|
||||||
if not sig_ref.grid:
|
|
||||||
sig_ref.grid = sig_ref.id
|
|
||||||
if sig_ref.grid and not sig_ref.latitude:
|
|
||||||
ll = locator_to_latlong(str(sig_ref.grid))
|
|
||||||
sig_ref.latitude = ll[0]
|
|
||||||
sig_ref.longitude = ll[1]
|
|
||||||
|
|
||||||
elif sig.upper() == "WAB" or sig.upper() == "WAI":
|
|
||||||
ll = wab_wai_square_to_lat_lon(ref_id)
|
|
||||||
if ll:
|
|
||||||
sig_ref.name = ref_id
|
|
||||||
try:
|
|
||||||
sig_ref.grid = latlong_to_locator(ll[0], ll[1], 6)
|
|
||||||
sig_ref.latitude = ll[0]
|
|
||||||
sig_ref.longitude = ll[1]
|
|
||||||
except:
|
|
||||||
logging.warning("Invalid lat/lon received for WAB/WAI reference")
|
|
||||||
|
|
||||||
except ConnectionError:
|
|
||||||
logging.warning("Connection error when looking up sig_ref info for " + sig + " ref " + ref_id)
|
|
||||||
except (ConnectTimeout, ReadTimeout):
|
|
||||||
logging.warning(f"Timeout when looking up sig_ref info for " + sig + " ref " + ref_id)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.error("Exception when looking up sig_ref info for " + sig + " ref " + ref_id, exc_info=True)
|
logging.error("Exception when looking up sig_ref info for " + sig + " ref " + ref_id, exc_info=True)
|
||||||
return sig_ref
|
return sig_ref
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ class StatusReporter:
|
|||||||
DATA_STORE.status_data["sig_ref_data_providers"] = list(
|
DATA_STORE.status_data["sig_ref_data_providers"] = list(
|
||||||
map(lambda p: {"sig_name": p.sig_name, "enabled": p.enabled, "status": p.status,
|
map(lambda p: {"sig_name": p.sig_name, "enabled": p.enabled, "status": p.status,
|
||||||
"last_updated": p.last_update_time.replace(
|
"last_updated": p.last_update_time.replace(
|
||||||
tzinfo=pytz.UTC).timestamp() if p.last_update_time.year > 2000 else 0},
|
tzinfo=pytz.UTC).timestamp() if p.last_update_time.year > 2000 else 0,
|
||||||
|
"reference_count": p.reference_count},
|
||||||
self._sig_ref_data_providers))
|
self._sig_ref_data_providers))
|
||||||
DATA_STORE.status_data["webserver"] = {"status": self._web_server.web_server_metrics["status"],
|
DATA_STORE.status_data["webserver"] = {"status": self._web_server.web_server_metrics["status"],
|
||||||
"last_api_access": self._web_server.web_server_metrics[
|
"last_api_access": self._web_server.web_server_metrics[
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ class Alert:
|
|||||||
def infer_missing(self, credentials=None):
|
def infer_missing(self, credentials=None):
|
||||||
"""Infer missing parameters where possible"""
|
"""Infer missing parameters where possible"""
|
||||||
|
|
||||||
|
try:
|
||||||
# If we somehow don't have a start time, set it to zero so it sorts off the bottom of any list but
|
# If we somehow don't have a start time, set it to zero so it sorts off the bottom of any list but
|
||||||
# clients can still reliably parse it as a number.
|
# clients can still reliably parse it as a number.
|
||||||
if not self.start_time:
|
if not self.start_time:
|
||||||
@@ -128,6 +130,9 @@ class Alert:
|
|||||||
self.dx_names = list(
|
self.dx_names = list(
|
||||||
map(lambda c: lookup_helper.infer_name_from_callsign_online_lookup(c, credentials), self.dx_calls))
|
map(lambda c: lookup_helper.infer_name_from_callsign_online_lookup(c, credentials), self.dx_calls))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Exception while inferring missing data from spot", e, exc_info=True)
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
"""JSON serialise"""
|
"""JSON serialise"""
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ class Spot:
|
|||||||
def infer_missing(self, credentials=None):
|
def infer_missing(self, credentials=None):
|
||||||
"""Infer missing parameters where possible"""
|
"""Infer missing parameters where possible"""
|
||||||
|
|
||||||
|
try:
|
||||||
# If we somehow don't have a spot time, set it to zero so it sorts off the bottom of any list but
|
# If we somehow don't have a spot time, set it to zero so it sorts off the bottom of any list but
|
||||||
# clients can still reliably parse it as a number.
|
# clients can still reliably parse it as a number.
|
||||||
if not self.time:
|
if not self.time:
|
||||||
@@ -437,6 +438,9 @@ class Spot:
|
|||||||
self.de_longitude = latlon[1]
|
self.de_longitude = latlon[1]
|
||||||
self.de_grid = lookup_helper.infer_grid_from_callsign_dxcc(self.de_call)
|
self.de_grid = lookup_helper.infer_grid_from_callsign_dxcc(self.de_call)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Exception while inferring missing data from spot", e, exc_info=True)
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
"""JSON serialise"""
|
"""JSON serialise"""
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -72,7 +72,8 @@ class WebServer:
|
|||||||
{"sse_spot_broadcaster": self._spot_broadcaster, **handler_opts}),
|
{"sse_spot_broadcaster": self._spot_broadcaster, **handler_opts}),
|
||||||
(r"/api/v1/alerts/stream", APIAlertsStreamHandler,
|
(r"/api/v1/alerts/stream", APIAlertsStreamHandler,
|
||||||
{"sse_alert_broadcaster": self._alert_broadcaster, **handler_opts}),
|
{"sse_alert_broadcaster": self._alert_broadcaster, **handler_opts}),
|
||||||
(r"/api/v1/solar", APISolarConditionsHandler, {"solar_conditions": self._data_store.solar, **handler_opts}),
|
(r"/api/v1/solar", APISolarConditionsHandler, {"solar_conditions": self._data_store.solar_conditions,
|
||||||
|
**handler_opts}),
|
||||||
(r"/api/v1/dxstats", APIDxStatsHandler, {"spots": self._data_store.spots, **handler_opts}),
|
(r"/api/v1/dxstats", APIDxStatsHandler, {"spots": self._data_store.spots, **handler_opts}),
|
||||||
(r"/api/v1/options", APIOptionsHandler, {"status_data": self._data_store.status_data, **handler_opts}),
|
(r"/api/v1/options", APIOptionsHandler, {"status_data": self._data_store.status_data, **handler_opts}),
|
||||||
(r"/api/v1/status", APIStatusHandler, {"status_data": self._data_store.status_data, **handler_opts}),
|
(r"/api/v1/status", APIStatusHandler, {"status_data": self._data_store.status_data, **handler_opts}),
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import csv
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class ARLHS(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for Amateur Radio Light House Society"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 30
|
||||||
|
SIG = "ARLHS"
|
||||||
|
DATA_URL = "https://www.gma.rocks/download/lighthouse.csv"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
|
||||||
|
if "ARLHS" in row:
|
||||||
|
ref_id = row["ARLHS"]
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["Name"] if "Name" in row else None,
|
||||||
|
url="https://www.cqgma.org/zinfo.php?ref=" + ref_id,
|
||||||
|
latitude=float(row["Latitude"]) if "Latitude" in row and row[
|
||||||
|
"Latitude"] != "" else None,
|
||||||
|
longitude=float(row["Longitude"]) if "Longitude" in row and row[
|
||||||
|
"Longitude"] != "" else None,
|
||||||
|
grid=row["Maidenhead Locator"])
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -3,11 +3,11 @@ from datetime import datetime
|
|||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
import requests
|
|
||||||
from requests import ReadTimeout
|
from requests import ReadTimeout
|
||||||
from requests.exceptions import ConnectionError, ConnectTimeout
|
from requests.exceptions import ConnectionError, ConnectTimeout
|
||||||
|
|
||||||
from core.constants import HTTP_HEADERS
|
from core.constants import HTTP_HEADERS
|
||||||
|
from core.url_data_cache import URL_DATA_CACHE
|
||||||
from sigrefdataproviders.sig_ref_data_provider import SIGRefDataProvider
|
from sigrefdataproviders.sig_ref_data_provider import SIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
|
|||||||
"""Generic SIG ref data provider class for providers that fetch their data from the web by downloading a file."""
|
"""Generic SIG ref data provider class for providers that fetch their data from the web by downloading a file."""
|
||||||
|
|
||||||
def __init__(self, sig_name, provider_config, url, poll_interval):
|
def __init__(self, sig_name, provider_config, url, poll_interval):
|
||||||
|
""" Set up the provider, note poll_interval is in *days*."""
|
||||||
super().__init__(sig_name, provider_config)
|
super().__init__(sig_name, provider_config)
|
||||||
self._url = url
|
self._url = url
|
||||||
self._poll_interval = poll_interval
|
self._poll_interval = poll_interval
|
||||||
@@ -25,7 +26,7 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
|
|||||||
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
|
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
|
||||||
# subsequent polls, so start() returns immediately and the application can continue starting.
|
# subsequent polls, so start() returns immediately and the application can continue starting.
|
||||||
logging.info(
|
logging.info(
|
||||||
"Set up query of " + self.sig_name + " SIG ref data every " + str(self._poll_interval) + " seconds.")
|
"Set up query of " + self.sig_name + " SIG ref data every " + str(self._poll_interval) + " days.")
|
||||||
self._thread = Thread(target=self._run, daemon=True)
|
self._thread = Thread(target=self._run, daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
|
|
||||||
@@ -35,14 +36,15 @@ class FileDownloadSIGRefDataProvider(SIGRefDataProvider):
|
|||||||
def _run(self):
|
def _run(self):
|
||||||
while True:
|
while True:
|
||||||
self._poll()
|
self._poll()
|
||||||
if self._stop_event.wait(timeout=self._poll_interval):
|
if self._stop_event.wait(timeout=self._poll_interval * 60 * 60 * 24):
|
||||||
break
|
break
|
||||||
|
|
||||||
def _poll(self):
|
def _poll(self):
|
||||||
try:
|
try:
|
||||||
# Request data from API
|
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
|
||||||
|
# caching, but just so continual restarts of the software during testing don't hammer the servers.
|
||||||
logging.debug("Downloading " + self.sig_name + " SIG ref data...")
|
logging.debug("Downloading " + self.sig_name + " SIG ref data...")
|
||||||
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
|
http_response = URL_DATA_CACHE.get(self._url, headers=HTTP_HEADERS)
|
||||||
# Check response code was good
|
# Check response code was good
|
||||||
if http_response.ok:
|
if http_response.ok:
|
||||||
# Pass off to the subclass for processing
|
# Pass off to the subclass for processing
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import csv
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class GMA(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for Global Mountain Activity"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 30
|
||||||
|
SIG = "GMA"
|
||||||
|
DATA_URL = "https://www.gma.rocks/download/summits.csv"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
|
||||||
|
ref_id = row["Reference"]
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["Name"] if "Name" in row else None,
|
||||||
|
url="https://www.cqgma.org/zinfo.php?ref=" + ref_id,
|
||||||
|
latitude=float(row["Latitude"]) if "Latitude" in row and row[
|
||||||
|
"Latitude"] != "" else None,
|
||||||
|
longitude=float(row["Longitude"]) if "Longitude" in row and row[
|
||||||
|
"Longitude"] != "" else None,
|
||||||
|
grid=row["Maidenhead Locator"])
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import csv
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class ILLW(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for International Lighthouse & Lightship Weekend"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 30
|
||||||
|
SIG = "ILLW"
|
||||||
|
DATA_URL = "https://www.gma.rocks/download/lighthouse.csv"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
|
||||||
|
if "ILLW" in row:
|
||||||
|
ref_id = row["ILLW"]
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["Name"] if "Name" in row else None,
|
||||||
|
url="https://www.cqgma.org/zinfo.php?ref=" + ref_id,
|
||||||
|
latitude=float(row["Latitude"]) if "Latitude" in row and row[
|
||||||
|
"Latitude"] != "" else None,
|
||||||
|
longitude=float(row["Longitude"]) if "Longitude" in row and row[
|
||||||
|
"Longitude"] != "" else None,
|
||||||
|
grid=row["Maidenhead Locator"])
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class IOTA(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for Islands on the Air"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 365
|
||||||
|
SIG = "IOTA"
|
||||||
|
DATA_URL = "https://www.iota-world.org/islands-on-the-air/downloads/download-file.html?path=groups.json"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
data = http_response.json()
|
||||||
|
if isinstance(data, list):
|
||||||
|
for ref in data:
|
||||||
|
ref_id = ref["refno"]
|
||||||
|
latitude = float(ref["latitude_min"]) + float(ref["latitude_max"]) / 2.0
|
||||||
|
longitude = float(ref["longitude_min"]) + float(ref["longitude_max"]) / 2.0
|
||||||
|
grid = None
|
||||||
|
try:
|
||||||
|
grid = latlong_to_locator(latitude, longitude, 6)
|
||||||
|
except ValueError:
|
||||||
|
logging.debug(f"Error converting lat/lon to locator for an IOTA reference %f %f", latitude, longitude)
|
||||||
|
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=ref["name"],
|
||||||
|
grid=grid, latitude=latitude, longitude=longitude)
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -7,12 +7,12 @@ from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownload
|
|||||||
class LLOTA(FileDownloadSIGRefDataProvider):
|
class LLOTA(FileDownloadSIGRefDataProvider):
|
||||||
"""SIG ref data provider for Lagos y Lagunas on the Air"""
|
"""SIG ref data provider for Lagos y Lagunas on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_SEC = 7 * 24 * 60 * 60 # 7 days
|
POLL_INTERVAL_DAYS = 7
|
||||||
SIG = "LLOTA"
|
SIG = "LLOTA"
|
||||||
DATA_URL = "https://llota.app/api/public/references"
|
DATA_URL = "https://llota.app/api/public/references"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = {}
|
new_data = {}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class LocalFileSIGRefDataProvider(SIGRefDataProvider):
|
|||||||
self._path = path
|
self._path = path
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
logging.info("Loading " + self.sig_name + " SIG ref data from file.")
|
logging.debug("Loading " + self.sig_name + " SIG ref data from file.")
|
||||||
try:
|
try:
|
||||||
new_data = self._file_to_data(self._path)
|
new_data = self._file_to_data(self._path)
|
||||||
if new_data:
|
if new_data:
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import csv
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class MOTA(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for Mills on the Air"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 30
|
||||||
|
SIG = "MOTA"
|
||||||
|
DATA_URL = "https://www.gma.rocks/download/mills.csv"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
|
||||||
|
ref_id = row["Reference"]
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["Name"] if "Name" in row else None,
|
||||||
|
url="https://www.cqgma.org/zinfo.php?ref=" + ref_id,
|
||||||
|
latitude=float(row["Latitude"]) if "Latitude" in row and row[
|
||||||
|
"Latitude"] != "" else None,
|
||||||
|
longitude=float(row["Longitude"]) if "Longitude" in row and row[
|
||||||
|
"Longitude"] != "" else None,
|
||||||
|
grid=row["Maidenhead Locator"])
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import csv
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class POTA(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for Parks on the Air"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 7
|
||||||
|
SIG = "POTA"
|
||||||
|
DATA_URL = "https://pota.app/all_parks_ext.csv"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
||||||
|
ref_id = row["reference"]
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["name"] if "name" in row else None,
|
||||||
|
url="https://pota.app/#/park/" + ref_id,
|
||||||
|
grid=row["grid"] if "grid" in row else None,
|
||||||
|
latitude=float(row["latitude"]) if "latitude" in row and row[
|
||||||
|
"latitude"] != "" else None,
|
||||||
|
longitude=float(row["longitude"]) if "longitude" in row and row[
|
||||||
|
"longitude"] != "" else None)
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
@@ -16,6 +17,7 @@ class SIGRefDataProvider:
|
|||||||
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||||
self.last_spot_time = datetime.min.replace(tzinfo=pytz.UTC)
|
self.last_spot_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||||
self.status = "Not Started" if self.enabled else "Disabled"
|
self.status = "Not Started" if self.enabled else "Disabled"
|
||||||
|
self.reference_count = 0
|
||||||
|
|
||||||
# Create an empty dict to store data if one doesn't already exist
|
# Create an empty dict to store data if one doesn't already exist
|
||||||
if not sig_name in DATA_STORE.sigrefs:
|
if not sig_name in DATA_STORE.sigrefs:
|
||||||
@@ -38,3 +40,5 @@ class SIGRefDataProvider:
|
|||||||
objects."""
|
objects."""
|
||||||
|
|
||||||
DATA_STORE.sigrefs[self.sig_name] = new_data
|
DATA_STORE.sigrefs[self.sig_name] = new_data
|
||||||
|
self.reference_count = len(new_data)
|
||||||
|
logging.info(f"Loaded %d references for %s into the data store.", self.reference_count, self.sig_name)
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownload
|
|||||||
class SIOTA(FileDownloadSIGRefDataProvider):
|
class SIOTA(FileDownloadSIGRefDataProvider):
|
||||||
"""SIG ref data provider for Silos on the Air"""
|
"""SIG ref data provider for Silos on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_SEC = 30 * 24 * 60 * 60 # 30 days
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "SIOTA"
|
SIG = "SIOTA"
|
||||||
DATA_URL = "https://www.silosontheair.com/data/silos.csv"
|
DATA_URL = "https://www.silosontheair.com/data/silos.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = {}
|
new_data = {}
|
||||||
for row in csv.DictReader(http_response.content.decode().splitlines()):
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
||||||
ref_id = row["SILO_CODE"]
|
ref_id = row["SILO_CODE"]
|
||||||
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["NAME"] if "NAME" in row else None,
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["NAME"] if "NAME" in row else None,
|
||||||
grid=row["LOCATOR"] if "LOCATOR" in row else None,
|
grid=row["LOCATOR"] if "LOCATOR" in row else None,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import csv
|
||||||
|
|
||||||
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class SOTA(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for Summits on the Air"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 30
|
||||||
|
SIG = "SOTA"
|
||||||
|
DATA_URL = "https://storage.sota.org.uk/summitslist.csv"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()[1:]):
|
||||||
|
ref_id = row["SummitCode"]
|
||||||
|
latitude = float(row["Latitude"]) if "Latitude" in row and row["Latitude"] != "" else None
|
||||||
|
longitude = float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["SummitName"] if "SummitName" in row else None,
|
||||||
|
url="https://www.sotadata.org.uk/en/summit/" + ref_id,
|
||||||
|
latitude=latitude,
|
||||||
|
longitude=longitude,
|
||||||
|
activation_score=int(row["Points"]) if "Points" in row else None)
|
||||||
|
if latitude and longitude:
|
||||||
|
new_data[ref_id].grid = latlong_to_locator(latitude, longitude, 6)
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -4,11 +4,11 @@ from data.sig_ref import SIGRef
|
|||||||
from sigrefdataproviders.local_file_sig_ref_data_provider import LocalFileSIGRefDataProvider
|
from sigrefdataproviders.local_file_sig_ref_data_provider import LocalFileSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
class TOTA(LocalFileSIGRefDataProvider):
|
class Toilets(LocalFileSIGRefDataProvider):
|
||||||
"""SIG ref data provider for Toilets on the Air"""
|
"""SIG ref data provider for Toilets on the Air"""
|
||||||
|
|
||||||
SIG = "TOTA"
|
SIG = "Toilets"
|
||||||
PATH = "datafiles/tota.csv"
|
PATH = "datafiles/toilets.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.PATH)
|
super().__init__(self.SIG, provider_config, self.PATH)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import csv
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class Towers(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for Towers on the Air"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 30
|
||||||
|
SIG = "Towers"
|
||||||
|
DATA_URL = "https://wwtota.com/servis/generate_csv.php?ref=&filter=all"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
|
||||||
|
ref_id = row["Ref"]
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["Nazev"] if "Nazev" in row else None,
|
||||||
|
url="https://wwtota.com/seznam/karta_rozhledny.php?ref=" + ref_id,
|
||||||
|
grid=row["Lokator"] if "Lokator" in row and row["Lokator"] != "" else None,
|
||||||
|
latitude=float(row["Lat"]) if "Lat" in row and row["Lat"] != "" else None,
|
||||||
|
longitude=float(row["Lon"]) if "Lon" in row and row["Lon"] != "" else None)
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import csv
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyhamtools.locator import latlong_to_locator
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class WCA(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for World Castles Award"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 30
|
||||||
|
SIG = "WCA"
|
||||||
|
DATA_URL = "https://polo.ham2k.com/data/activities/wca/all-castles.csv"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
||||||
|
ref_id = row["REF"]
|
||||||
|
|
||||||
|
coordsStr = row["COORDINATES"]
|
||||||
|
latitude = None
|
||||||
|
longitude = None
|
||||||
|
grid = None
|
||||||
|
try:
|
||||||
|
if coordsStr:
|
||||||
|
split = coordsStr.split(", ")
|
||||||
|
latitude = float(split[0])
|
||||||
|
longitude = float(split[1])
|
||||||
|
grid = latlong_to_locator(latitude, longitude)
|
||||||
|
except ValueError:
|
||||||
|
logging.debug(f"Encountered dodgy formatting in WCA CSV, skipping location data for %s", ref_id)
|
||||||
|
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["CLEAN NAME"] if "CLEAN NAME" in row else None,
|
||||||
|
url="https://www.cqgma.org/zinfo.php?ref=" + ref_id,
|
||||||
|
latitude=latitude,
|
||||||
|
longitude=longitude,
|
||||||
|
grid=grid)
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -5,12 +5,12 @@ from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownload
|
|||||||
class WOTA(FileDownloadSIGRefDataProvider):
|
class WOTA(FileDownloadSIGRefDataProvider):
|
||||||
"""SIG ref data provider for Wainwrights on the Air"""
|
"""SIG ref data provider for Wainwrights on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_SEC = 365 * 24 * 60 * 60 # 365 days
|
POLL_INTERVAL_DAYS = 365
|
||||||
SIG = "WOTA"
|
SIG = "WOTA"
|
||||||
DATA_URL = "https://www.wota.org.uk/mapping/data/summits.json"
|
DATA_URL = "https://www.wota.org.uk/mapping/data/summits.json"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = {}
|
new_data = {}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import csv
|
||||||
|
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownloadSIGRefDataProvider
|
||||||
|
|
||||||
|
|
||||||
|
class WWBOTA(FileDownloadSIGRefDataProvider):
|
||||||
|
"""SIG ref data provider for Worldwide Bunkers on the Air"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_DAYS = 30
|
||||||
|
SIG = "WWBOTA"
|
||||||
|
DATA_URL = "https://api.wwbota.org/bunkers/?format=CSV"
|
||||||
|
|
||||||
|
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 = {}
|
||||||
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
||||||
|
ref_id = row["Reference"]
|
||||||
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["Name"] if "Name" in row else None,
|
||||||
|
url="https://bunkerwiki.org/?s=" + ref_id if ref_id.startswith("B/G") else None,
|
||||||
|
grid=row["Locator"] if "Locator" in row and row["Locator"] != "" else None,
|
||||||
|
latitude=float(row["Lat"]) if "Lat" in row and row["Lat"] != "" else None,
|
||||||
|
longitude=float(row["Long"]) if "Long" in row and row["Long"] != "" else None)
|
||||||
|
|
||||||
|
return new_data
|
||||||
@@ -7,16 +7,16 @@ from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownload
|
|||||||
class WWFF(FileDownloadSIGRefDataProvider):
|
class WWFF(FileDownloadSIGRefDataProvider):
|
||||||
"""SIG ref data provider for Worldwide Flora & Fauna"""
|
"""SIG ref data provider for Worldwide Flora & Fauna"""
|
||||||
|
|
||||||
POLL_INTERVAL_SEC = 30 * 24 * 60 * 60 # 30 days
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "WWFF"
|
SIG = "WWFF"
|
||||||
DATA_URL = "https://wwff.co/wwff-data/wwff_directory.csv"
|
DATA_URL = "https://wwff.co/wwff-data/wwff_directory.csv"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = {}
|
new_data = {}
|
||||||
for row in csv.DictReader(http_response.content.decode().splitlines()):
|
for row in csv.DictReader(http_response.content.decode("utf-8-sig").splitlines()):
|
||||||
ref_id = row["reference"]
|
ref_id = row["reference"]
|
||||||
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["name"] if "name" in row else None,
|
new_data[ref_id] = SIGRef(sig=self.SIG, id=ref_id, name=row["name"] if "name" in row else None,
|
||||||
url="https://wwff.co/directory/?showRef=" + ref_id,
|
url="https://wwff.co/directory/?showRef=" + ref_id,
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ from sigrefdataproviders.file_download_sig_ref_data_provider import FileDownload
|
|||||||
class ZLOTA(FileDownloadSIGRefDataProvider):
|
class ZLOTA(FileDownloadSIGRefDataProvider):
|
||||||
"""SIG ref data provider for New Zealand on the Air"""
|
"""SIG ref data provider for New Zealand on the Air"""
|
||||||
|
|
||||||
POLL_INTERVAL_SEC = 30 * 24 * 60 * 60 # 30 days
|
POLL_INTERVAL_DAYS = 30
|
||||||
SIG = "ZLOTA"
|
SIG = "ZLOTA"
|
||||||
DATA_URL = "https://ontheair.nz/assets/assets.json"
|
DATA_URL = "https://ontheair.nz/assets/assets.json"
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_SEC)
|
super().__init__(self.SIG, provider_config, self.DATA_URL, self.POLL_INTERVAL_DAYS)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = {}
|
new_data = {}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class Towers(HTTPSpotProvider):
|
|||||||
dx_call=source_spot["call"].upper(),
|
dx_call=source_spot["call"].upper(),
|
||||||
freq=likely_freq,
|
freq=likely_freq,
|
||||||
comment=source_spot["comment"],
|
comment=source_spot["comment"],
|
||||||
sig="WWTOTA",
|
sig="Towers",
|
||||||
sig_refs=[SIGRef(id=source_spot["ref"], sig="WWTOTA")],
|
sig_refs=[SIGRef(id=source_spot["ref"], sig="Towers")],
|
||||||
time=datetime.strptime(response_json["updated"][:10] + source_spot["time"],
|
time=datetime.strptime(response_json["updated"][:10] + source_spot["time"],
|
||||||
"%Y-%m-%d%H:%M").timestamp())
|
"%Y-%m-%d%H:%M").timestamp())
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class WOTA(HTTPSpotProvider):
|
|||||||
|
|
||||||
def _http_response_to_spots(self, http_response):
|
def _http_response_to_spots(self, http_response):
|
||||||
new_spots = []
|
new_spots = []
|
||||||
rss = cast(RSS, Parser.parse(http_response.content.decode()))
|
rss = cast(RSS, Parser.parse(http_response.content.decode("utf-8-sig")))
|
||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_spot in rss.channel.items:
|
for source_spot in rss.channel.items:
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from spotproviders.websocket_spot_provider import WebsocketSpotProvider
|
|||||||
class XOTA(WebsocketSpotProvider):
|
class XOTA(WebsocketSpotProvider):
|
||||||
"""Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/
|
"""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 which we can
|
The provider typically doesn't give us a lat/lon or SIG explicitly, so our own config provides a SIG which we can
|
||||||
then use for lookups. This functionality is implemented for TOTA events, of which there are
|
then use for lookups. This functionality is implemented for Toilets on the Air events, of which there are
|
||||||
several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA, which is why we also
|
several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA, which is why we also
|
||||||
provide a sig_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01" at C3 might become
|
provide a sig_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01" at C3 might become
|
||||||
"C3 T-01". This allows us to provide location lookups for TOTA at several conferences."""
|
"C3 T-01". This allows us to provide location lookups for TOTA at several conferences."""
|
||||||
|
|||||||
@@ -796,7 +796,7 @@ components:
|
|||||||
- HEMA
|
- HEMA
|
||||||
- WCA
|
- WCA
|
||||||
- MOTA
|
- MOTA
|
||||||
- SiOTA
|
- SIOTA
|
||||||
- ARLHS
|
- ARLHS
|
||||||
- ILLW
|
- ILLW
|
||||||
- ZLOTA
|
- ZLOTA
|
||||||
@@ -1742,6 +1742,10 @@ components:
|
|||||||
The last time at which this provider received data, UTC seconds since UNIX epoch. If this
|
The last time at which this provider received data, UTC seconds since UNIX epoch. If this
|
||||||
is zero, the provider has never updated.
|
is zero, the provider has never updated.
|
||||||
example: 1759579508
|
example: 1759579508
|
||||||
|
reference_count:
|
||||||
|
type: number
|
||||||
|
description: The number of references fetched using this provider.
|
||||||
|
example: 1234
|
||||||
|
|
||||||
SpotList:
|
SpotList:
|
||||||
type: array
|
type: array
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ function loadStatus() {
|
|||||||
<div class="col"><strong>${p["sig_name"]}</strong></div>
|
<div class="col"><strong>${p["sig_name"]}</strong></div>
|
||||||
<div class="col">Status: ${p["status"]}</div>
|
<div class="col">Status: ${p["status"]}</div>
|
||||||
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
|
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
|
||||||
|
<div class="col">Reference count: ${(p["enabled"] && p["reference_count"] > 0) ? p["reference_count"] : "N/A"}</div>
|
||||||
</div>`);
|
</div>`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user