5 Commits
32 changed files with 472 additions and 320 deletions
+15 -8
View File
@@ -41,16 +41,23 @@ class HTTPAlertProvider(AlertProvider):
# Request data from API # Request data from API
logging.debug("Polling " + self.name + " alert API...") logging.debug("Polling " + self.name + " alert API...")
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30)) http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
# Pass off to the subclass for processing # Check response code was good
new_alerts = self._http_response_to_alerts(http_response) if http_response.ok:
# Submit the new alerts for processing. There might not be any alerts for the less popular programs. # Pass off to the subclass for processing
if new_alerts: new_alerts = self._http_response_to_alerts(http_response)
self._submit_batch(new_alerts) # Submit the new alerts for processing. There might not be any alerts for the less popular programs.
if new_alerts:
self._submit_batch(new_alerts)
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug("Received data from " + self.name + " alert API.") logging.debug("Received data from " + self.name + " alert API.")
else:
self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when calling {self.name} alerts API.")
except ConnectionError:
logging.warning(f"Connection error when accessing {self.name} alerts API.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception in HTTP JSON Alert Provider (" + self.name + ")") logging.exception("Exception in HTTP JSON Alert Provider (" + self.name + ")")
+4
View File
@@ -215,6 +215,10 @@ allow-spotting: true
# your log quickly on a popular server. # your log quickly on a popular server.
log-web-requests: false log-web-requests: false
# Minimum severity of log statements to print to the system log. Defaults to INFO, change to DEBUG if you need more
# information.
log-level: INFO
# Options for the web UI. # Options for the web UI.
web-ui-options: web-ui-options:
spot-count: [ 10, 25, 50, 100 ] spot-count: [ 10, 25, 50, 100 ]
+5 -2
View File
@@ -8,8 +8,11 @@ from requests_cache import CachedSession
# of time has passed. This is used throughout Spothole to cache data that does not change # of time has passed. This is used throughout Spothole to cache data that does not change
# rapidly. The ThreadSafeSession construct here protects it against some multithreading # rapidly. The ThreadSafeSession construct here protects it against some multithreading
# contention weirdness we sometimes used to see on startup where the cache was hammered # contention weirdness we sometimes used to see on startup where the cache was hammered
# pretty hard. # pretty hard. The expanded list of allowable_codes ensures we also cache and return 400-type
_session = CachedSession("cache/semi_static_url_data_cache", expire_after=timedelta(days=30)) # responses, e.g "this SOTA summit ref doesn't actually exist", to avoid hammering remote
# servers for data they've told us they can't provide.
_session = CachedSession("cache/semi_static_url_data_cache", expire_after=timedelta(days=30),
allowable_codes=(200, 400, 401, 403, 404))
_lock = threading.Lock() _lock = threading.Lock()
+1
View File
@@ -22,6 +22,7 @@ WEB_SERVER_PORT = config["web-server-port"]
ALLOW_SPOTTING = config["allow-spotting"] ALLOW_SPOTTING = config["allow-spotting"]
WEB_UI_OPTIONS = config["web-ui-options"] WEB_UI_OPTIONS = config["web-ui-options"]
API_ONLY_MODE = config.get("api-only-mode", False) API_ONLY_MODE = config.get("api-only-mode", False)
LOG_LEVEL = config.get("log-level", "INFO")
LOG_WEB_REQUESTS = config.get("log-web-requests", False) LOG_WEB_REQUESTS = config.get("log-web-requests", False)
# For ease of config, each spot provider owns its own config about whether it should be enabled by default in the web UI # For ease of config, each spot provider owns its own config about whether it should be enabled by default in the web UI
+61 -27
View File
@@ -5,7 +5,6 @@ import re
import urllib.parse import urllib.parse
from datetime import timedelta from datetime import timedelta
import requests
import xmltodict import xmltodict
from diskcache import Cache from diskcache import Cache
from pyhamtools import LookupLib, Callinfo, callinfo from pyhamtools import LookupLib, Callinfo, callinfo
@@ -143,13 +142,19 @@ class LookupHelper:
try: try:
logging.info("Downloading Country-files.com cty.plist...") logging.info("Downloading Country-files.com cty.plist...")
response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.country-files.com/cty/cty.plist", response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.country-files.com/cty/cty.plist",
headers=HTTP_HEADERS).text headers=HTTP_HEADERS)
with open(self._country_files_cty_plist_download_location, "w") as f: if response.ok:
f.write(response) with open(self._country_files_cty_plist_download_location, "w") as f:
f.flush() f.write(response.text)
return True f.flush()
return True
else:
logging.warning(f"HTTP {response.status_code} when downloading Country-files.com cty.plist.")
return False
except ConnectionError:
logging.warning(f"Connection error when downloading Clublog cty.xml.")
except Exception as e: except Exception as e:
logging.error("Exception when downloading Clublog cty.xml", e) logging.error("Exception when downloading Clublog cty.xml", e)
return False return False
@@ -161,13 +166,19 @@ class LookupHelper:
logging.info("Downloading dxcc.json...") logging.info("Downloading dxcc.json...")
response = SEMI_STATIC_URL_DATA_CACHE.get( response = SEMI_STATIC_URL_DATA_CACHE.get(
"https://raw.githubusercontent.com/k0swe/dxcc-json/refs/heads/main/dxcc.json", "https://raw.githubusercontent.com/k0swe/dxcc-json/refs/heads/main/dxcc.json",
headers=HTTP_HEADERS).text headers=HTTP_HEADERS)
with open(self._dxcc_json_download_location, "w") as f: if response.ok:
f.write(response) with open(self._dxcc_json_download_location, "w") as f:
f.flush() f.write(response.text)
return True f.flush()
return True
else:
logging.warning(f"HTTP {response.status_code} when downloading dxcc.json.")
return False
except ConnectionError:
logging.warning(f"Connection error when downloading dxcc.json.")
except Exception as e: except Exception as e:
logging.error("Exception when downloading dxcc.json", e) logging.error("Exception when downloading dxcc.json", e)
return False return False
@@ -496,19 +507,35 @@ class LookupHelper:
for lookup_call in calls_to_try: for lookup_call in calls_to_try:
try: try:
lookup_response = requests.get( response = SEMI_STATIC_URL_DATA_CACHE.get(
self._qrz_base_url + "?s=" + session_key + "&callsign=" + urllib.parse.quote_plus(lookup_call), self._qrz_base_url + "?s=" + session_key + "&callsign=" + urllib.parse.quote_plus(lookup_call),
headers=HTTP_HEADERS, timeout=10).content headers=HTTP_HEADERS, timeout=10)
raw = xmltodict.parse(lookup_response).get("QRZDatabase", {}).get("Callsign") if response.ok:
if raw: qrz_response = xmltodict.parse(response.content).get("QRZDatabase", {})
data = _normalize_qrz_data(raw) if qrz_response:
self._qrz_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds if "Callsign" in qrz_response:
return data data = _normalize_qrz_data(qrz_response.get("Callsign"))
self._qrz_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
return data
elif "Session" in qrz_response and "Error" in qrz_response.get("Session"):
# Errors here are normally just "callsign not in database", no need to log that ourselves
# above debug level.
logging.debug("QRZ returned an error looking up callsign %s: %s", lookup_call,
qrz_response.get("Session").get("Error"))
elif not response.from_cache:
logging.warning("QRZ returned a malformed response looking up callsign %s", lookup_call)
elif not response.from_cache:
logging.warning("HTTP %d looking up callsign %s using QRZ", lookup_call)
except (KeyError, ValueError): except (KeyError, ValueError):
continue continue
except ConnectionError:
logging.warning(f"Connection error when looking up callsign %s using QRZ", lookup_call)
continue
except Exception: except Exception:
logging.error("Exception when looking up QRZ data") logging.error("Exception when looking up callsign %s using QRZ", lookup_call, exc_info=True)
return None continue
# Not found in QRZ; cache None so we don't keep retrying # Not found in QRZ; cache None so we don't keep retrying
self._qrz_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds self._qrz_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
@@ -552,17 +579,24 @@ class LookupHelper:
for lookup_call in calls_to_try: for lookup_call in calls_to_try:
try: try:
lookup_data = SEMI_STATIC_URL_DATA_CACHE.get( response = SEMI_STATIC_URL_DATA_CACHE.get(
self._hamqth_base_url + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus( self._hamqth_base_url + "?id=" + session_id + "&callsign=" + urllib.parse.quote_plus(
lookup_call) + "&prg=" + HAMQTH_PRG, headers=HTTP_HEADERS).content lookup_call) + "&prg=" + HAMQTH_PRG, headers=HTTP_HEADERS)
data = xmltodict.parse(lookup_data)["HamQTH"]["search"] if response.ok:
self._hamqth_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds data = xmltodict.parse(response.content)["HamQTH"]["search"]
return data self._hamqth_callsign_data_cache.add(call, data, expire=604800) # 1 week in seconds
return data
elif not response.from_cache:
logging.warning("HTTP %d looking up callsign %s using HamQTH", response.status_code, lookup_call)
except (KeyError, ValueError): except (KeyError, ValueError):
continue continue
except ConnectionError:
logging.warning(f"Connection error when looking up callsign %s using HamQTH", lookup_call)
continue
except Exception: except Exception:
logging.error("Exception when looking up HamQTH data") logging.error("Exception when looking up callsign %s using HamQTH", lookup_call, exc_info=True)
return None continue
# Not found in HamQTH; cache None so we don't keep retrying # Not found in HamQTH; cache None so we don't keep retrying
self._hamqth_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds self._hamqth_callsign_data_cache.add(call, None, expire=604800) # 1 week in seconds
+155 -109
View File
@@ -45,144 +45,181 @@ def populate_sig_ref_info(sig_ref):
try: try:
if sig.upper() == "POTA": if sig.upper() == "POTA":
response = SEMI_STATIC_URL_DATA_CACHE.get("https://api.pota.app/park/" + ref_id, headers=HTTP_HEADERS) response = SEMI_STATIC_URL_DATA_CACHE.get("https://api.pota.app/park/" + ref_id, headers=HTTP_HEADERS)
if not response.ok: if response.ok:
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) logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
data = response.json() if response.ok else None
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 sig.upper() == "SOTA": elif sig.upper() == "SOTA":
response = SEMI_STATIC_URL_DATA_CACHE.get("https://api-db2.sota.org.uk/api/summits/" + ref_id, response = SEMI_STATIC_URL_DATA_CACHE.get("https://api-db2.sota.org.uk/api/summits/" + ref_id,
headers=HTTP_HEADERS) headers=HTTP_HEADERS)
if not response.ok: if response.ok:
data = response.json()
if data:
sig_ref.name = data["name"] if "name" in data else None
sig_ref.url = "https://www.sotadata.org.uk/en/summit/" + ref_id
sig_ref.grid = data["locator"] if "locator" 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
sig_ref.activation_score = data["points"] if "points" 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) logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
data = response.json() if response.ok else None
if data:
sig_ref.name = data["name"] if "name" in data else None
sig_ref.url = "https://www.sotadata.org.uk/en/summit/" + ref_id
sig_ref.grid = data["locator"] if "locator" 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
sig_ref.activation_score = data["points"] if "points" in data else None
elif sig.upper() == "WWBOTA": elif sig.upper() == "WWBOTA":
response = SEMI_STATIC_URL_DATA_CACHE.get("https://api.wwbota.org/bunkers/" + ref_id, response = SEMI_STATIC_URL_DATA_CACHE.get("https://api.wwbota.org/bunkers/" + ref_id,
headers=HTTP_HEADERS) headers=HTTP_HEADERS)
if not response.ok: if response.ok:
data = response.json()
if data:
sig_ref.name = data["name"] if "name" in data else None
sig_ref.url = "https://bunkerwiki.org/?s=" + ref_id if ref_id.startswith("B/G") else None
sig_ref.grid = data["locator"] if "locator" in data else None
sig_ref.latitude = data["lat"] if "lat" in data else None
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) logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
data = response.json() if response.ok else None
if data:
sig_ref.name = data["name"] if "name" in data else None
sig_ref.url = "https://bunkerwiki.org/?s=" + ref_id if ref_id.startswith("B/G") else None
sig_ref.grid = data["locator"] if "locator" in data else None
sig_ref.latitude = data["lat"] if "lat" in data else None
sig_ref.longitude = data["long"] if "long" in data else None
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() == "GMA" or sig.upper() == "ARLHS" or sig.upper() == "ILLW" or sig.upper() == "WCA" or sig.upper() == "MOTA" or sig.upper() == "IOTA":
response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.cqgma.org/api/ref/?" + ref_id, response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.cqgma.org/api/ref/?" + ref_id,
headers=HTTP_HEADERS) headers=HTTP_HEADERS)
if not response.ok: if response.ok:
data = response.json()
if data:
sig_ref.name = data["name"] if "name" in data else None
sig_ref.url = "https://www.cqgma.org/zinfo.php?ref=" + ref_id
sig_ref.grid = data["locator"] if "locator" 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 via GMA", sig, ref_id)
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)
data = response.json() if response.ok else None
if data:
sig_ref.name = data["name"] if "name" in data else None
sig_ref.url = "https://www.cqgma.org/zinfo.php?ref=" + ref_id
sig_ref.grid = data["locator"] if "locator" 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 sig.upper() == "WWFF": elif sig.upper() == "WWFF":
wwff_response = SEMI_STATIC_URL_DATA_CACHE.get("https://wwff.co/wwff-data/wwff_directory.csv", response = SEMI_STATIC_URL_DATA_CACHE.get("https://wwff.co/wwff-data/wwff_directory.csv",
headers=HTTP_HEADERS) headers=HTTP_HEADERS)
if not wwff_response.ok: if response.ok:
logging.warning("HTTP %d looking up %s ref %s", wwff_response.status_code, sig, ref_id) wwff_index = {row["reference"]: row for row in csv.DictReader(response.content.decode().splitlines())}
return sig_ref row = wwff_index.get(ref_id)
wwff_index = {row["reference"]: row for row in csv.DictReader(wwff_response.content.decode().splitlines())} if row:
row = wwff_index.get(ref_id) sig_ref.name = row["name"] if "name" in row else None
if row: sig_ref.url = "https://wwff.co/directory/?showRef=" + ref_id
sig_ref.name = row["name"] if "name" in row else None sig_ref.grid = row["iaruLocator"] if "iaruLocator" in row and row["iaruLocator"] != "-" else None
sig_ref.url = "https://wwff.co/directory/?showRef=" + ref_id sig_ref.latitude = float(row["latitude"]) if "latitude" in row and row["latitude"] != "-" else None
sig_ref.grid = row["iaruLocator"] if "iaruLocator" in row and row["iaruLocator"] != "-" else None sig_ref.longitude = float(row["longitude"]) if "longitude" in row and row[
sig_ref.latitude = float(row["latitude"]) if "latitude" in row and row["latitude"] != "-" else None "longitude"] != "-" else None
sig_ref.longitude = float(row["longitude"]) if "longitude" in row and row["longitude"] != "-" else None elif not response.from_cache:
logging.warning("WWFF database did not contain data for ref %s", 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() == "SIOTA": elif sig.upper() == "SIOTA":
siota_response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.silosontheair.com/data/silos.csv", response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.silosontheair.com/data/silos.csv",
headers=HTTP_HEADERS) headers=HTTP_HEADERS)
if not siota_response.ok: if response.ok:
logging.warning("HTTP %d looking up %s ref %s", siota_response.status_code, sig, ref_id) siota_index = {row["SILO_CODE"]: row for row in
return sig_ref csv.DictReader(response.content.decode().splitlines())}
siota_index = {row["SILO_CODE"]: row for row in row = siota_index.get(ref_id)
csv.DictReader(siota_response.content.decode().splitlines())} if row:
row = siota_index.get(ref_id) sig_ref.name = row["NAME"] if "NAME" in row else None
if row: sig_ref.grid = row["LOCATOR"] if "LOCATOR" in row else None
sig_ref.name = row["NAME"] if "NAME" in row else None sig_ref.latitude = float(row["LAT"]) if "LAT" in row else None
sig_ref.grid = row["LOCATOR"] if "LOCATOR" in row else None sig_ref.longitude = float(row["LNG"]) if "LNG" in row else None
sig_ref.latitude = float(row["LAT"]) if "LAT" in row else None elif not response.from_cache:
sig_ref.longitude = float(row["LNG"]) if "LNG" in row else None logging.warning("SIOTA database did not contain data for ref %s", 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() == "WOTA": elif sig.upper() == "WOTA":
response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.wota.org.uk/mapping/data/summits.json", response = SEMI_STATIC_URL_DATA_CACHE.get("https://www.wota.org.uk/mapping/data/summits.json",
headers=HTTP_HEADERS) headers=HTTP_HEADERS)
if not response.ok: if response.ok:
data = response.json()
if data:
for feature in data.get("features", []):
if feature["properties"]["wotaId"] == ref_id:
sig_ref.name = feature["properties"]["title"]
# Fudge WOTA URLs. Outlying fell (LDO) URLs don't match their ID numbers but require 214 to be
# added to them
sig_ref.url = "https://www.wota.org.uk/MM_" + ref_id
if ref_id.upper().startswith("LDO-"):
number = int(ref_id.upper().replace("LDO-", ""))
sig_ref.url = "https://www.wota.org.uk/MM_LDO-" + str(number + 214)
sig_ref.grid = feature["properties"]["qthLocator"]
sig_ref.latitude = feature["geometry"]["coordinates"][1]
sig_ref.longitude = feature["geometry"]["coordinates"][0]
break
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) logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
data = response.json() if response.ok else None
if data:
for feature in data.get("features", []):
if feature["properties"]["wotaId"] == ref_id:
sig_ref.name = feature["properties"]["title"]
# Fudge WOTA URLs. Outlying fell (LDO) URLs don't match their ID numbers but require 214 to be
# added to them
sig_ref.url = "https://www.wota.org.uk/MM_" + ref_id
if ref_id.upper().startswith("LDO-"):
number = int(ref_id.upper().replace("LDO-", ""))
sig_ref.url = "https://www.wota.org.uk/MM_LDO-" + str(number + 214)
sig_ref.grid = feature["properties"]["qthLocator"]
sig_ref.latitude = feature["geometry"]["coordinates"][1]
sig_ref.longitude = feature["geometry"]["coordinates"][0]
break
elif sig.upper() == "ZLOTA": elif sig.upper() == "ZLOTA":
response = SEMI_STATIC_URL_DATA_CACHE.get("https://ontheair.nz/assets/assets.json", headers=HTTP_HEADERS) response = SEMI_STATIC_URL_DATA_CACHE.get("https://ontheair.nz/assets/assets.json", headers=HTTP_HEADERS)
if not response.ok: if response.ok:
data = response.json()
if isinstance(data, list):
for asset in data:
if asset["code"] == ref_id:
sig_ref.name = asset["name"]
sig_ref.url = "https://ontheair.nz/assets/" + ref_id.replace("/", "_")
try:
sig_ref.grid = latlong_to_locator(asset["y"], asset["x"], 6)
except:
logging.debug("Invalid lat/lon received for reference")
sig_ref.latitude = asset["y"]
sig_ref.longitude = asset["x"]
break
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) logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
data = response.json() if response.ok else None
if isinstance(data, list):
for asset in data:
if asset["code"] == ref_id:
sig_ref.name = asset["name"]
sig_ref.url = "https://ontheair.nz/assets/ZLI_OT-030" + ref_id.replace("/", "_")
try:
sig_ref.grid = latlong_to_locator(asset["y"], asset["x"], 6)
except:
logging.debug("Invalid lat/lon received for reference")
sig_ref.latitude = asset["y"]
sig_ref.longitude = asset["x"]
break
elif sig.upper() == "BOTA": elif sig.upper() == "BOTA":
if not sig_ref.name: if not sig_ref.name:
sig_ref.name = sig_ref.id sig_ref.name = sig_ref.id
sig_ref.url = "https://www.beachesontheair.com/beaches/" + sig_ref.name.lower().replace(" ", "-") sig_ref.url = "https://www.beachesontheair.com/beaches/" + sig_ref.name.lower().replace(" ", "-")
elif sig.upper() == "LLOTA": elif sig.upper() == "LLOTA":
response = SEMI_STATIC_URL_DATA_CACHE.get("https://llota.app/api/public/references", response = SEMI_STATIC_URL_DATA_CACHE.get("https://llota.app/api/public/references",
headers=HTTP_HEADERS) headers=HTTP_HEADERS)
if not response.ok: if response.ok:
data = response.json()
if isinstance(data, list):
for ref in data:
if ref["reference_code"] == ref_id:
sig_ref.name = str(ref["name"])
sig_ref.url = "https://llota.app/list/ref/" + ref_id
sig_ref.grid = str(ref["grid_locator"])
ll = locator_to_latlong(sig_ref.grid)
sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1]
break
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) logging.warning("HTTP %d looking up %s ref %s", response.status_code, sig, ref_id)
data = response.json() if response.ok else None
if isinstance(data, list):
for ref in data:
if ref["reference_code"] == ref_id:
sig_ref.name = str(ref["name"])
sig_ref.url = "https://llota.app/list/ref/" + ref_id
sig_ref.grid = str(ref["grid_locator"])
ll = locator_to_latlong(sig_ref.grid)
sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1]
break
elif sig.upper() == "WWTOTA": elif sig.upper() == "WWTOTA":
if not sig_ref.name: if not sig_ref.name:
sig_ref.name = sig_ref.id sig_ref.name = sig_ref.id
sig_ref.url = "https://wwtota.com/seznam/karta_rozhledny.php?ref=" + str(sig_ref.name) sig_ref.url = "https://wwtota.com/seznam/karta_rozhledny.php?ref=" + str(sig_ref.name)
elif sig.upper() == "TILES": elif sig.upper() == "TILES":
# Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same # Tiles on the Air just uses Maidenhead 6-digit squares, so ID, Name and Grid are all the same
if not sig_ref.name: if not sig_ref.name:
@@ -193,6 +230,7 @@ def populate_sig_ref_info(sig_ref):
ll = locator_to_latlong(str(sig_ref.grid)) ll = locator_to_latlong(str(sig_ref.grid))
sig_ref.latitude = ll[0] sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1] sig_ref.longitude = ll[1]
elif sig.upper() == "WAB" or sig.upper() == "WAI": elif sig.upper() == "WAB" or sig.upper() == "WAI":
ll = wab_wai_square_to_lat_lon(ref_id) ll = wab_wai_square_to_lat_lon(ref_id)
if ll: if ll:
@@ -202,21 +240,29 @@ def populate_sig_ref_info(sig_ref):
sig_ref.latitude = ll[0] sig_ref.latitude = ll[0]
sig_ref.longitude = ll[1] sig_ref.longitude = ll[1]
except: except:
logging.debug("Invalid lat/lon received for reference") logging.warning("Invalid lat/lon received for WAB/WAI reference")
elif sig.upper() == "DME": elif sig.upper() == "DME":
# Zero-pad to 5 digits to match our source data # Zero-pad to 5 digits to match our source data
row = _DME_INDEX.get(ref_id.zfill(5)) row = _DME_INDEX.get(ref_id.zfill(5))
if row: if row:
sig_ref.name = row["NOMBRE_ACTUAL"] + ", " + row["PROVINCIA"] sig_ref.name = row["NOMBRE_ACTUAL"] + ", " + row["PROVINCIA"]
sig_ref.latitude = float(row["LATITUD_ETRS89_REGCAN95"].replace(",", ".")) if row.get("LATITUD_ETRS89_REGCAN95") else None sig_ref.latitude = float(row["LATITUD_ETRS89_REGCAN95"].replace(",", ".")) if row.get(
sig_ref.longitude = float(row["LONGITUD_ETRS89_REGCAN95"].replace(",", ".")) if row.get("LONGITUD_ETRS89_REGCAN95") else None "LATITUD_ETRS89_REGCAN95") else None
sig_ref.longitude = float(row["LONGITUD_ETRS89_REGCAN95"].replace(",", ".")) if row.get(
"LONGITUD_ETRS89_REGCAN95") else None
if sig_ref.latitude and sig_ref.longitude: if sig_ref.latitude and sig_ref.longitude:
try: try:
sig_ref.grid = latlong_to_locator(sig_ref.latitude, sig_ref.longitude, 6) sig_ref.grid = latlong_to_locator(sig_ref.latitude, sig_ref.longitude, 6)
except Exception: except Exception:
logging.debug("Invalid lat/lon received for reference") logging.warning("Invalid lat/lon received for DME reference")
else:
logging.warning("DME database did not contain data for ref %s", ref_id)
except ConnectionError:
logging.warning("Connection error when looking up sig_ref info for " + sig + " ref " + ref_id)
except Exception: except Exception:
logging.warning("Failed to look 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
+1 -1
View File
@@ -127,7 +127,7 @@ class APISpotHandler(tornado.web.RequestHandler):
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception as e: except Exception as e:
logging.error(e) logging.error("Exception when handling client request to add spot API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
+1 -2
View File
@@ -59,11 +59,10 @@ class APIAlertsHandler(tornado.web.RequestHandler):
self.write(safe_json_dumps(data)) self.write(safe_json_dumps(data))
self.set_status(200) self.set_status(200)
except ValueError as e: except ValueError as e:
logging.error(e)
self.write(safe_json_dumps("Bad request - " + str(e))) self.write(safe_json_dumps("Bad request - " + str(e)))
self.set_status(400) self.set_status(400)
except Exception as e: except Exception as e:
logging.error(e) logging.error("Exception when handling client request to alerts API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
+30 -22
View File
@@ -1,4 +1,5 @@
import json import json
import logging
from collections import Counter from collections import Counter
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from typing import Any
@@ -9,6 +10,7 @@ from tornado import httputil
from tornado.web import Application from tornado.web import Application
from core.prometheus_metrics_handler import api_requests_counter from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"] CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"]
BANDS = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"] BANDS = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"]
@@ -29,29 +31,35 @@ class APIDxStatsHandler(tornado.web.RequestHandler):
self._web_server_metrics = web_server_metrics self._web_server_metrics = web_server_metrics
def get(self): def get(self):
self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) try:
self._web_server_metrics["api_access_counter"] += 1 self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC)
self._web_server_metrics["status"] = "OK" self._web_server_metrics["api_access_counter"] += 1
api_requests_counter.inc() self._web_server_metrics["status"] = "OK"
api_requests_counter.inc()
one_hour_ago = (datetime.now(pytz.UTC) - timedelta(hours=1)).timestamp() one_hour_ago = (datetime.now(pytz.UTC) - timedelta(hours=1)).timestamp()
counts = Counter() counts = Counter()
for key in self._spots.iterkeys(): for key in self._spots.iterkeys():
spot = self._spots.get(key) spot = self._spots.get(key)
if spot is None: if spot is None:
continue continue
if not spot.time or spot.time < one_hour_ago: if not spot.time or spot.time < one_hour_ago:
continue continue
if spot.de_continent in CONTINENTS_SET and spot.dx_continent in CONTINENTS_SET and spot.band in BANDS_SET: if spot.de_continent in CONTINENTS_SET and spot.dx_continent in CONTINENTS_SET and spot.band in BANDS_SET:
counts[spot.de_continent, spot.dx_continent, spot.band] += 1 counts[spot.de_continent, spot.dx_continent, spot.band] += 1
result = { result = {
de: {dx: {band: counts[de, dx, band] for band in BANDS} for dx in CONTINENTS} de: {dx: {band: counts[de, dx, band] for band in BANDS} for dx in CONTINENTS}
for de in CONTINENTS for de in CONTINENTS
} }
self.write(json.dumps(result)) self.write(json.dumps(result))
self.set_status(200) self.set_status(200)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception as e:
logging.error("Exception when handling client request to dx stats API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
+3 -3
View File
@@ -74,7 +74,7 @@ class APILookupCallHandler(tornado.web.RequestHandler):
self.set_status(422) self.set_status(422)
except Exception as e: except Exception as e:
logging.error(e) logging.error("Exception when handling client request to call lookup API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
@@ -126,7 +126,7 @@ class APILookupSIGRefHandler(tornado.web.RequestHandler):
self.set_status(422) self.set_status(422)
except Exception as e: except Exception as e:
logging.error(e) logging.error("Exception when handling client request to sig ref lookup API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
@@ -188,7 +188,7 @@ class APILookupGridHandler(tornado.web.RequestHandler):
self.set_status(422) self.set_status(422)
except Exception as e: except Exception as e:
logging.error(e) logging.error("Exception when handling client request to grid ref lookup API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
+33 -26
View File
@@ -1,3 +1,4 @@
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@@ -25,31 +26,37 @@ class APIOptionsHandler(tornado.web.RequestHandler):
self._web_server_metrics = web_server_metrics self._web_server_metrics = web_server_metrics
def get(self): def get(self):
# Metrics try:
self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) # Metrics
self._web_server_metrics["api_access_counter"] += 1 self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC)
self._web_server_metrics["status"] = "OK" self._web_server_metrics["api_access_counter"] += 1
api_requests_counter.inc() self._web_server_metrics["status"] = "OK"
api_requests_counter.inc()
options = {"bands": BANDS, options = {"bands": BANDS,
"modes": ALL_MODES, "modes": ALL_MODES,
"mode_types": MODE_TYPES, "mode_types": MODE_TYPES,
"sigs": SIGS, "sigs": SIGS,
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle things that aren't even available. # Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle things that aren't even available.
"spot_sources": list( "spot_sources": list(
map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["spot_providers"]))), map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["spot_providers"]))),
"alert_sources": list( "alert_sources": list(
map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["alert_providers"]))), map(lambda p: p["name"], filter(lambda p: p["enabled"], self._status_data["alert_providers"]))),
"continents": CONTINENTS, "continents": CONTINENTS,
"propagation_modes": list(PROPAGATION_MODES.values()), "propagation_modes": list(PROPAGATION_MODES.values()),
"max_spot_age": MAX_SPOT_AGE, "max_spot_age": MAX_SPOT_AGE,
"spot_allowed": ALLOW_SPOTTING} "spot_allowed": ALLOW_SPOTTING}
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from # If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
# one of our proviers. # one of our proviers.
if ALLOW_SPOTTING: if ALLOW_SPOTTING:
options["spot_sources"].append("API") options["spot_sources"].append("API")
self.write(safe_json_dumps(options)) self.write(safe_json_dumps(options))
self.set_status(200) self.set_status(200)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception as e:
logging.error("Exception when handling client request to options API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
+17 -9
View File
@@ -1,3 +1,4 @@
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@@ -7,6 +8,7 @@ from tornado import httputil
from tornado.web import Application from tornado.web import Application
from core.prometheus_metrics_handler import api_requests_counter from core.prometheus_metrics_handler import api_requests_counter
from core.utils import safe_json_dumps
class APISolarConditionsHandler(tornado.web.RequestHandler): class APISolarConditionsHandler(tornado.web.RequestHandler):
@@ -22,13 +24,19 @@ class APISolarConditionsHandler(tornado.web.RequestHandler):
self._web_server_metrics = web_server_metrics self._web_server_metrics = web_server_metrics
def get(self): def get(self):
# Metrics try:
self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) # Metrics
self._web_server_metrics["api_access_counter"] += 1 self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC)
self._web_server_metrics["status"] = "OK" self._web_server_metrics["api_access_counter"] += 1
api_requests_counter.inc() self._web_server_metrics["status"] = "OK"
api_requests_counter.inc()
self.write(self._solar_conditions.to_json()) self.write(self._solar_conditions.to_json())
self.set_status(200) self.set_status(200)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception as e:
logging.error("Exception when handling client request to solar conditions API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
+1 -2
View File
@@ -59,11 +59,10 @@ class APISpotsHandler(tornado.web.RequestHandler):
self.write(safe_json_dumps(data)) self.write(safe_json_dumps(data))
self.set_status(200) self.set_status(200)
except ValueError as e: except ValueError as e:
logging.error(e)
self.write(safe_json_dumps("Bad request - " + str(e))) self.write(safe_json_dumps("Bad request - " + str(e)))
self.set_status(400) self.set_status(400)
except Exception as e: except Exception as e:
logging.error(e) logging.error("Excedption when handling client request to spots API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred.")) self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500) self.set_status(500)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
+16 -9
View File
@@ -1,3 +1,4 @@
import logging
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
@@ -23,13 +24,19 @@ class APIStatusHandler(tornado.web.RequestHandler):
self._web_server_metrics = web_server_metrics self._web_server_metrics = web_server_metrics
def get(self): def get(self):
# Metrics try:
self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC) # Metrics
self._web_server_metrics["api_access_counter"] += 1 self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC)
self._web_server_metrics["status"] = "OK" self._web_server_metrics["api_access_counter"] += 1
api_requests_counter.inc() self._web_server_metrics["status"] = "OK"
api_requests_counter.inc()
self.write(safe_json_dumps(self._status_data)) self.write(safe_json_dumps(self._status_data))
self.set_status(200) self.set_status(200)
self.set_header("Cache-Control", "no-store") self.set_header("Cache-Control", "no-store")
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
except Exception as e:
logging.error("Exception when handling client request to status API: %s", e, exc_info=True)
self.write(safe_json_dumps("Error - an internal server error occurred."))
self.set_status(500)
+8 -3
View File
@@ -127,10 +127,15 @@ class GIROIonosonde(SolarConditionsProvider):
from_str = from_time.strftime("%Y.%m.%d+%H:%M:%S") from_str = from_time.strftime("%Y.%m.%d+%H:%M:%S")
to_str = to_time.strftime("%Y.%m.%d+%H:%M:%S") to_str = to_time.strftime("%Y.%m.%d+%H:%M:%S")
url = f"{LGDC_URL}?ursiCode={ursi}&charName=foF2,MUFD,fmin&DMUF=3000&fromDate={from_str}&toDate={to_str}" url = f"{LGDC_URL}?ursiCode={ursi}&charName=foF2,MUFD,fmin&DMUF=3000&fromDate={from_str}&toDate={to_str}"
response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15)) try:
if response.status_code != 200: http_response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15))
if not http_response.ok:
logging.warning(f"HTTP {http_response.status_code} when calling Giro ionosonde API.")
return None, None, None
return self._parse_all(http_response.text)
except ConnectionError:
logging.warning("Connection error when accessing Giro ionosonde API.")
return None, None, None return None, None, None
return self._parse_all(response.text)
@staticmethod @staticmethod
def _parse_all(text): def _parse_all(text):
-4
View File
@@ -18,10 +18,6 @@ class HamQSL(HTTPSolarConditionsProvider):
super().__init__(provider_config, URL, POLL_INTERVAL) super().__init__(provider_config, URL, POLL_INTERVAL)
def _http_response_to_solar_conditions(self, http_response): def _http_response_to_solar_conditions(self, http_response):
if http_response.status_code != 200:
logging.warning("HamQSL solar conditions API returned HTTP " + str(http_response.status_code))
return None
root = ElementTree.fromstring(http_response.text) root = ElementTree.fromstring(http_response.text)
sd = root.find("solardata") sd = root.find("solardata")
if sd is None: if sd is None:
@@ -39,13 +39,20 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
try: try:
logging.debug("Polling " + self.name + " solar conditions API...") logging.debug("Polling " + self.name + " solar conditions API...")
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30)) http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
new_data = self._http_response_to_solar_conditions(http_response) # Check response code was good
self.update_data(new_data) if http_response.ok:
new_data = self._http_response_to_solar_conditions(http_response)
self.update_data(new_data)
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug("Received data from " + self.name + " solar conditions API.") logging.debug("Received data from " + self.name + " solar conditions API.")
else:
self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when calling {self.name} solar conditions API.")
except ConnectionError:
logging.warning(f"Connection error when accessing {self.name} solar conditions API.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception in HTTP Solar Conditions Provider (" + self.name + ")") logging.exception("Exception in HTTP Solar Conditions Provider (" + self.name + ")")
+6 -4
View File
@@ -44,9 +44,9 @@ class KC2GProp(SolarConditionsProvider):
def _poll(self): def _poll(self):
try: try:
logging.debug("Polling KC2G ionosonde data...") logging.debug("Polling KC2G ionosonde data...")
response = requests.get(KC2G_URL, headers=HTTP_HEADERS, timeout=(5, 30)) http_response = requests.get(KC2G_URL, headers=HTTP_HEADERS, timeout=(5, 30))
if response.status_code != 200: if not http_response.ok:
logging.warning(f"KC2G ionosonde API returned HTTP {response.status_code}") logging.warning(f"HTTP {http_response.status_code} when calling KG2G ionosonde API.")
return return
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
@@ -57,7 +57,7 @@ class KC2GProp(SolarConditionsProvider):
ionosonde_data = dict(self._solar_conditions.ionosonde_data or {}) ionosonde_data = dict(self._solar_conditions.ionosonde_data or {})
updated_count = 0 updated_count = 0
for reading in response.json(): for reading in http_response.json():
station = reading.get("station", {}) station = reading.get("station", {})
ursi = station.get("code") ursi = station.get("code")
name = station.get("name") name = station.get("name")
@@ -115,6 +115,8 @@ class KC2GProp(SolarConditionsProvider):
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug(f"Updated KC2G ionosonde data for {updated_count} stations.") logging.debug(f"Updated KC2G ionosonde data for {updated_count} stations.")
except ConnectionError:
logging.warning("Connection error when accessing KC2G ionosonde API.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception in KC2G ionosonde data provider") logging.exception("Exception in KC2G ionosonde data provider")
@@ -80,10 +80,6 @@ class NOAA3dayForecast(HTTPSolarConditionsProvider):
return result if result else None return result if result else None
def _http_response_to_solar_conditions(self, http_response): def _http_response_to_solar_conditions(self, http_response):
if http_response.status_code != 200:
logging.warning("NOAA K-index forecast API returned HTTP " + str(http_response.status_code))
return None
lines = http_response.text.splitlines() lines = http_response.text.splitlines()
# Find the "NOAA Kp index breakdown" section header # Find the "NOAA Kp index breakdown" section header
+3 -3
View File
@@ -8,7 +8,7 @@ import sys
from diskcache import Cache from diskcache import Cache
from core.cleanup import CleanupTimer from core.cleanup import CleanupTimer
from core.config import config, SERVER_OWNER_CALLSIGN from core.config import config, SERVER_OWNER_CALLSIGN, LOG_LEVEL
from core.constants import SOFTWARE_VERSION from core.constants import SOFTWARE_VERSION
from core.lookup_helper import lookup_helper from core.lookup_helper import lookup_helper
from core.status_reporter import StatusReporter from core.status_reporter import StatusReporter
@@ -84,9 +84,9 @@ def get_solar_conditions_provider_from_config(config_providers_entry):
if __name__ == '__main__': if __name__ == '__main__':
# Set up logging # Set up logging
root = logging.getLogger() root = logging.getLogger()
root.setLevel(logging.INFO) root.setLevel(LOG_LEVEL)
handler = logging.StreamHandler(sys.stdout) handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.INFO) handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter("%(levelname)s : %(message)s") formatter = logging.Formatter("%(levelname)s : %(message)s")
handler.setFormatter(formatter) handler.setFormatter(formatter)
root.handlers.clear() root.handlers.clear()
+13 -5
View File
@@ -39,7 +39,7 @@ class GMA(HTTPSpotProvider):
de_call=source_spot["SPOTTER"].upper(), de_call=source_spot["SPOTTER"].upper(),
# Seen GMA spots with no frequency or with "QRT" in this field # Seen GMA spots with no frequency or with "QRT" in this field
freq=float(source_spot["QRG"]) * 1000 if ( freq=float(source_spot["QRG"]) * 1000 if (
source_spot["QRG"] != "" and source_spot["QRG"] != "QRT") else None, source_spot["QRG"] != "" and source_spot["QRG"] != "QRT") else None,
# Filter out some weird mode strings # Filter out some weird mode strings
mode=source_spot["MODE"].upper() if "<>" not in source_spot["MODE"] else None, mode=source_spot["MODE"].upper() if "<>" not in source_spot["MODE"] else None,
comment=source_spot["TEXT"], comment=source_spot["TEXT"],
@@ -58,8 +58,8 @@ class GMA(HTTPSpotProvider):
try: try:
ref_response = SEMI_STATIC_URL_DATA_CACHE.get(self.REF_INFO_URL_ROOT + source_spot["REF"], ref_response = SEMI_STATIC_URL_DATA_CACHE.get(self.REF_INFO_URL_ROOT + source_spot["REF"],
headers=HTTP_HEADERS) headers=HTTP_HEADERS)
# Sometimes this is blank, so handle that # Sometimes this is blank even if it's a 200 response, so handle that
if ref_response.text is not None and ref_response.text != "": if ref_response.ok and ref_response.text is not None and ref_response.text != "":
ref_info = ref_response.json() 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 # 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 # spots come through with reftype=POTA or reftype=WWFF. SOTA is harder to figure out because both SOTA
@@ -94,9 +94,17 @@ class GMA(HTTPSpotProvider):
spot.sig_refs[0].sig = ref_info["reftype"] spot.sig_refs[0].sig = ref_info["reftype"]
spot.sig = ref_info["reftype"] spot.sig = ref_info["reftype"]
# Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do # Add to our list. Don't worry about de-duping, removing old spots etc. at this point;
# that for us. # other code will do that for us.
new_spots.append(spot) new_spots.append(spot)
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"]}")
else:
logging.warning(
f"GMA API returned a malformed response when looking up ref {source_spot["REF"]}")
except: except:
logging.warning("Exception when looking up " + self.REF_INFO_URL_ROOT + source_spot[ logging.warning("Exception when looking up " + self.REF_INFO_URL_ROOT + source_spot[
"REF"] + ", ignoring this spot for now") "REF"] + ", ignoring this spot for now")
+32 -28
View File
@@ -1,3 +1,4 @@
import logging
import re import re
from datetime import datetime from datetime import datetime
@@ -35,34 +36,37 @@ class HEMA(HTTPSpotProvider):
new_spots = [] new_spots = []
# OK, if the spot seed actually changed, now we make the real request for data. # OK, if the spot seed actually changed, now we make the real request for data.
if spot_seed_changed: if spot_seed_changed:
source_data = requests.get(self.SPOTS_URL, headers=HTTP_HEADERS, timeout=(5, 30)) try:
source_data_items = source_data.text.split("=") source_data = requests.get(self.SPOTS_URL, headers=HTTP_HEADERS, timeout=(5, 30))
# Iterate through source data items. source_data_items = source_data.text.split("=")
for source_spot in source_data_items: # Iterate through source data items.
spot_items = source_spot.split(";") for source_spot in source_data_items:
# Any line with less than 9 items is not a proper spot line spot_items = source_spot.split(";")
if len(spot_items) >= 9: # Any line with less than 9 items is not a proper spot line
# Fiddle with some data to extract bits we need. Freq/mode and spotter/comment come in combined fields. if len(spot_items) >= 9:
freq_mode_match = re.search(self.FREQ_MODE_PATTERN, spot_items[5]) # Fiddle with some data to extract bits we need. Freq/mode and spotter/comment come in combined fields.
spotter_comment_match = re.search(self.SPOTTER_COMMENT_PATTERN, spot_items[6]) freq_mode_match = re.search(self.FREQ_MODE_PATTERN, spot_items[5])
if not freq_mode_match or not spotter_comment_match: spotter_comment_match = re.search(self.SPOTTER_COMMENT_PATTERN, spot_items[6])
continue if not freq_mode_match or not spotter_comment_match:
continue
# Convert to our spot format # Convert to our spot format
spot = Spot(source=self.name, spot = Spot(source=self.name,
dx_call=spot_items[2].upper(), dx_call=spot_items[2].upper(),
de_call=spotter_comment_match.group(1).upper(), de_call=spotter_comment_match.group(1).upper(),
freq=float(freq_mode_match.group(1)) * 1000000, freq=float(freq_mode_match.group(1)) * 1000000,
mode=freq_mode_match.group(2).upper(), mode=freq_mode_match.group(2).upper(),
comment=spotter_comment_match.group(2), comment=spotter_comment_match.group(2),
sig="HEMA", sig="HEMA",
sig_refs=[SIGRef(id=spot_items[3].upper(), sig="HEMA", name=spot_items[4])], 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( time=datetime.strptime(spot_items[0], "%d/%m/%Y %H:%M").replace(
tzinfo=pytz.UTC).timestamp(), tzinfo=pytz.UTC).timestamp(),
dx_latitude=float(spot_items[7]), dx_latitude=float(spot_items[7]),
dx_longitude=float(spot_items[8])) 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 # Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other
# that for us. # code will do that for us.
new_spots.append(spot) new_spots.append(spot)
except ConnectionError:
logging.warning("Connection error when accessing HEMA spots API.")
return new_spots return new_spots
+15 -8
View File
@@ -41,16 +41,23 @@ class HTTPSpotProvider(SpotProvider):
# Request data from API # Request data from API
logging.debug("Polling " + self.name + " spot API...") logging.debug("Polling " + self.name + " spot API...")
http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30)) http_response = requests.get(self._url, headers=HTTP_HEADERS, timeout=(5, 30))
# Pass off to the subclass for processing # Check response code was good
new_spots = self._http_response_to_spots(http_response) if http_response.ok:
# Submit the new spots for processing. There might not be any spots for the less popular programs. # Pass off to the subclass for processing
if new_spots: new_spots = self._http_response_to_spots(http_response)
self._submit_batch(new_spots) # Submit the new spots for processing. There might not be any spots for the less popular programs.
if new_spots:
self._submit_batch(new_spots)
self.status = "OK" self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC) self.last_update_time = datetime.now(pytz.UTC)
logging.debug("Received data from " + self.name + " spot API.") logging.debug("Received data from " + self.name + " spot API.")
else:
self.status = "Error"
logging.warning(f"HTTP {http_response.status_code} when calling {self.name} spot API.")
except ConnectionError:
logging.warning(f"Connection error when accessing {self.name} spots API.")
except Exception: except Exception:
self.status = "Error" self.status = "Error"
logging.exception("Exception in HTTP JSON Spot Provider (" + self.name + ")") logging.exception("Exception in HTTP JSON Spot Provider (" + self.name + ")")
+25 -21
View File
@@ -1,3 +1,4 @@
import logging
from datetime import datetime from datetime import datetime
import requests import requests
@@ -33,26 +34,29 @@ class SOTA(HTTPSpotProvider):
new_spots = [] new_spots = []
# OK, if the epoch actually changed, now we make the real request for data. # OK, if the epoch actually changed, now we make the real request for data.
if epoch_changed: if epoch_changed:
source_data = requests.get(self.SPOTS_URL, headers=HTTP_HEADERS, timeout=(5, 30)).json() try:
# Iterate through source data source_data = requests.get(self.SPOTS_URL, headers=HTTP_HEADERS, timeout=(5, 30)).json()
for source_spot in source_data: # Iterate through source data
# Convert to our spot format for source_spot in source_data:
spot = Spot(source=self.name, # Convert to our spot format
source_id=source_spot["id"], spot = Spot(source=self.name,
dx_call=source_spot["activatorCallsign"].upper(), source_id=source_spot["id"],
dx_name=source_spot["activatorName"], dx_call=source_spot["activatorCallsign"].upper(),
de_call=source_spot["callsign"].upper(), dx_name=source_spot["activatorName"],
freq=(float(source_spot["frequency"]) * 1000000) if ( de_call=source_spot["callsign"].upper(),
source_spot["frequency"] is not None) else None, freq=(float(source_spot["frequency"]) * 1000000) if (
# Seen SOTA spots with no frequency! source_spot["frequency"] is not None) else None,
mode=source_spot["mode"].upper(), # Seen SOTA spots with no frequency!
comment=source_spot["comments"], mode=source_spot["mode"].upper(),
sig="SOTA", comment=source_spot["comments"],
sig_refs=[SIGRef(id=source_spot["summitCode"], sig="SOTA", name=source_spot["summitName"], sig="SOTA",
activation_score=source_spot["points"])], sig_refs=[SIGRef(id=source_spot["summitCode"], sig="SOTA", name=source_spot["summitName"],
time=datetime.fromisoformat(source_spot["timeStamp"].replace("Z", "+00:00")).timestamp()) 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 # Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do
# that for us. # that for us.
new_spots.append(spot) new_spots.append(spot)
except ConnectionError:
logging.warning("Connection error when accessing SOTA spots API")
return new_spots return new_spots
+1 -1
View File
@@ -76,7 +76,7 @@
</div> </div>
<script src="/js/add-spot.js?v=1783702256"></script> <script src="/js/add-spot.js?v=1784968398"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active"); $("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -75,7 +75,7 @@
</div> </div>
<script src="/js/alerts.js?v=1783702256"></script> <script src="/js/alerts.js?v=1784968398"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active"); $("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -75,8 +75,8 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/spotsbandsandmap.js?v=1783702256"></script> <script src="/js/spotsbandsandmap.js?v=1784968398"></script>
<script src="/js/bands.js?v=1783702256"></script> <script src="/js/bands.js?v=1784968398"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-bands").addClass("active"); $("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+5 -5
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/css/style.css?v=1783702256" type="text/css"> <link rel="stylesheet" href="/css/style.css?v=1784968398" type="text/css">
<link href="/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet"> <link href="/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet"> <link href="/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/vendor/css/solid-6.7.2.min.css" rel="stylesheet"> <link href="/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -10,10 +10,10 @@
<script src="/vendor/js/bootstrap-5.3.8.bundle.min.js"></script> <script src="/vendor/js/bootstrap-5.3.8.bundle.min.js"></script>
<script src="/vendor/js/tinycolor2-1.6.0.min.js"></script> <script src="/vendor/js/tinycolor2-1.6.0.min.js"></script>
<script src="/js/utils.js?v=1783702256"></script> <script src="/js/utils.js?v=1784968398"></script>
<script src="/js/ui-ham.js?v=1783702256"></script> <script src="/js/ui-ham.js?v=1784968398"></script>
<script src="/js/geo.js?v=1783702256"></script> <script src="/js/geo.js?v=1784968398"></script>
<script src="/js/common.js?v=1783702256"></script> <script src="/js/common.js?v=1784968398"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">
+1 -1
View File
@@ -284,7 +284,7 @@
</div> </div>
<script src="/vendor/js/chart-4.4.9.umd.min.js"></script> <script src="/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/js/conditions.js?v=1783702256"></script> <script src="/js/conditions.js?v=1784968398"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active"); $("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -95,8 +95,8 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/spotsbandsandmap.js?v=1783702255"></script> <script src="/js/spotsbandsandmap.js?v=1784968398"></script>
<script src="/js/map.js?v=1783702255"></script> <script src="/js/map.js?v=1784968398"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-map").addClass("active"); $("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -116,8 +116,8 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/spotsbandsandmap.js?v=1783702255"></script> <script src="/js/spotsbandsandmap.js?v=1784968398"></script>
<script src="/js/spots.js?v=1783702255"></script> <script src="/js/spots.js?v=1784968398"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-spots").addClass("active"); $("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -59,7 +59,7 @@
</div> </div>
</div> </div>
<script src="/js/status.js?v=1783702256"></script> <script src="/js/status.js?v=1784968398"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#nav-link-status").addClass("active"); $("#nav-link-status").addClass("active");