mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-04-29 18:25:58 +00:00
Fetch solar conditions from HamQSL #92
This commit is contained in:
@@ -301,6 +301,7 @@ To navigate your way around the source code, this list may help.
|
|||||||
* `/data` - Data storage classes
|
* `/data` - Data storage classes
|
||||||
* `/spotproviders` - Classes providing spots by accessing the APIs of other services
|
* `/spotproviders` - Classes providing spots by accessing the APIs of other services
|
||||||
* `/alertproviders` - Classes providing alerts by accessing the APIs of other services
|
* `/alertproviders` - Classes providing alerts by accessing the APIs of other services
|
||||||
|
* `/solarconditionsproviders` - Classes providing solar and propagation by accessing the APIs of other services
|
||||||
* `/server` - Classes for running Spothole's own web server
|
* `/server` - Classes for running Spothole's own web server
|
||||||
|
|
||||||
*Templates*
|
*Templates*
|
||||||
@@ -355,4 +356,6 @@ The software uses a number of Python libraries as listed in `requirements.txt`,
|
|||||||
|
|
||||||
Particular thanks go to country-files.com for providing country lookup data for amateur radio, to K0SWE for [this JSON-formatted DXCC data](https://github.com/k0swe/dxcc-json/), and to the developers of `pyhamtools` for making it easy to use country-files.com data as well as QRZ.com and Clublog lookup.
|
Particular thanks go to country-files.com for providing country lookup data for amateur radio, to K0SWE for [this JSON-formatted DXCC data](https://github.com/k0swe/dxcc-json/), and to the developers of `pyhamtools` for making it easy to use country-files.com data as well as QRZ.com and Clublog lookup.
|
||||||
|
|
||||||
|
Amateur radio clusters, outdoor programmes, propagation data providers etc. are almost all volunteer-run services that make no or little profit, and are done for the love of amateur radio. Services like Spothole, which build on top of them, are truly standing on the shoulders of giants. None of this would have been possible without the hard work and dedication of many other people within the amaetur radio community.
|
||||||
|
|
||||||
The project's name was suggested by Harm, DK4HAA. Thanks!
|
The project's name was suggested by Harm, DK4HAA. Thanks!
|
||||||
|
|||||||
@@ -128,7 +128,6 @@ spot-providers:
|
|||||||
sig: "TOTA"
|
sig: "TOTA"
|
||||||
locations-csv: "datafiles/39c3-tota.csv"
|
locations-csv: "datafiles/39c3-tota.csv"
|
||||||
|
|
||||||
|
|
||||||
# Alert providers to use. Same setup as the spot providers list above.
|
# Alert providers to use. Same setup as the spot providers list above.
|
||||||
alert-providers:
|
alert-providers:
|
||||||
-
|
-
|
||||||
@@ -160,6 +159,15 @@ alert-providers:
|
|||||||
name: "NG3K"
|
name: "NG3K"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
|
||||||
|
# Solar condition providers to use. These poll external APIs for solar propagation data (SFI, A/K indices, band
|
||||||
|
# conditions, etc.) and make it available via the /api/v1/solar endpoint.
|
||||||
|
solar-condition-providers:
|
||||||
|
-
|
||||||
|
class: "HamQSL"
|
||||||
|
name: "HamQSL"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
# Port to open the local web server on
|
# Port to open the local web server on
|
||||||
web-server-port: 8080
|
web-server-port: 8080
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class StatusReporter:
|
|||||||
"""Provides a timed update of the application's status data."""
|
"""Provides a timed update of the application's status data."""
|
||||||
|
|
||||||
def __init__(self, status_data, run_interval, web_server, cleanup_timer, spots, spot_providers, alerts,
|
def __init__(self, status_data, run_interval, web_server, cleanup_timer, spots, spot_providers, alerts,
|
||||||
alert_providers):
|
alert_providers, solar_condition_providers):
|
||||||
"""Constructor"""
|
"""Constructor"""
|
||||||
|
|
||||||
self._status_data = status_data
|
self._status_data = status_data
|
||||||
@@ -25,6 +25,7 @@ class StatusReporter:
|
|||||||
self._spot_providers = spot_providers
|
self._spot_providers = spot_providers
|
||||||
self._alerts = alerts
|
self._alerts = alerts
|
||||||
self._alert_providers = alert_providers
|
self._alert_providers = alert_providers
|
||||||
|
self._solar_condition_providers = solar_condition_providers
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self._stop_event = Event()
|
self._stop_event = Event()
|
||||||
self._startup_time = datetime.now(pytz.UTC)
|
self._startup_time = datetime.now(pytz.UTC)
|
||||||
@@ -70,6 +71,11 @@ class StatusReporter:
|
|||||||
"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},
|
||||||
self._alert_providers))
|
self._alert_providers))
|
||||||
|
self._status_data["solar_condition_providers"] = list(
|
||||||
|
map(lambda p: {"name": p.name, "enabled": p.enabled, "status": p.status,
|
||||||
|
"last_updated": p.last_update_time.replace(
|
||||||
|
tzinfo=pytz.UTC).timestamp() if p.last_update_time.year > 2000 else 0},
|
||||||
|
self._solar_condition_providers))
|
||||||
self._status_data["cleanup"] = {"status": self._cleanup_timer.status,
|
self._status_data["cleanup"] = {"status": self._cleanup_timer.status,
|
||||||
"last_ran": self._cleanup_timer.last_cleanup_time.replace(
|
"last_ran": self._cleanup_timer.last_cleanup_time.replace(
|
||||||
tzinfo=pytz.UTC).timestamp() if self._cleanup_timer.last_cleanup_time else 0}
|
tzinfo=pytz.UTC).timestamp() if self._cleanup_timer.last_cleanup_time else 0}
|
||||||
|
|||||||
69
data/solar_conditions.py
Normal file
69
data/solar_conditions.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import json
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HFBandCondition:
|
||||||
|
"""Data class representing HF propagation conditions for certain bands and time of day."""
|
||||||
|
|
||||||
|
# Band name, e.g. "80m-40m", "20m-17m", "10m-6m"
|
||||||
|
band: str = None
|
||||||
|
# Time of day: "day" or "night"
|
||||||
|
time: str = None
|
||||||
|
# Propagation condition: "Good", "Fair", or "Poor"
|
||||||
|
condition: str = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VHFCondition:
|
||||||
|
"""Data class representing a VHF propagation condition."""
|
||||||
|
|
||||||
|
# Phenomenon name, e.g. "E-Skip", "Sporadic E"
|
||||||
|
phenomenon: str = None
|
||||||
|
# Geographic location this applies to, e.g. "Europe", "N America"
|
||||||
|
location: str = None
|
||||||
|
# Condition description, e.g. "Band Closed", "Enhanced", "Good"
|
||||||
|
condition: str = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SolarConditions:
|
||||||
|
"""Data class representing current solar and propagation conditions."""
|
||||||
|
|
||||||
|
# Time the data was last updated at the source, UTC seconds since UNIX epoch
|
||||||
|
updated: float = None
|
||||||
|
# Solar Flux Index (SFI)
|
||||||
|
sfi: int = None
|
||||||
|
# A-index (daily geomagnetic activity)
|
||||||
|
a_index: int = None
|
||||||
|
# K-index (3-hour geomagnetic activity)
|
||||||
|
k_index: int = None
|
||||||
|
# X-ray flux class, e.g. "B2.3", "C1.0"
|
||||||
|
x_ray: str = None
|
||||||
|
# Proton flux
|
||||||
|
proton_flux: int = None
|
||||||
|
# Electron flux
|
||||||
|
electron_flux: int = None
|
||||||
|
# Aurora activity level
|
||||||
|
aurora: int = None
|
||||||
|
# Latitude in degrees of the aurora boundary
|
||||||
|
aurora_latitude: float = None
|
||||||
|
# Sunspot count
|
||||||
|
sunspots: int = None
|
||||||
|
# Solar wind speed in km/s
|
||||||
|
solar_wind: float = None
|
||||||
|
# Interplanetary magnetic field strength in nT
|
||||||
|
magnetic_field: float = None
|
||||||
|
# Geomagnetic field condition, e.g. "Quiet", "Unsettled", "Active", "Storm"
|
||||||
|
geomag_field: str = None
|
||||||
|
# Geomagnetic background noise level, e.g. "S0", "S1", "S2"
|
||||||
|
geomag_noise: str = None
|
||||||
|
# HF band propagation conditions
|
||||||
|
hf_conditions: list = None # list[HFBandCondition]
|
||||||
|
# VHF propagation phenomena
|
||||||
|
vhf_conditions: list = None # list[VHFCondition]
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
"""JSON serialise"""
|
||||||
|
|
||||||
|
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 194 KiB |
28
server/handlers/api/solar_conditions.py
Normal file
28
server/handlers/api/solar_conditions.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
import tornado
|
||||||
|
|
||||||
|
from core.prometheus_metrics_handler import api_requests_counter
|
||||||
|
from core.utils import serialize_everything
|
||||||
|
|
||||||
|
|
||||||
|
class APISolarConditionsHandler(tornado.web.RequestHandler):
|
||||||
|
"""API request handler for /api/v1/solar"""
|
||||||
|
|
||||||
|
def initialize(self, solar_conditions, web_server_metrics):
|
||||||
|
self._solar_conditions = solar_conditions
|
||||||
|
self._web_server_metrics = web_server_metrics
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
# Metrics
|
||||||
|
self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC)
|
||||||
|
self._web_server_metrics["api_access_counter"] += 1
|
||||||
|
self._web_server_metrics["status"] = "OK"
|
||||||
|
api_requests_counter.inc()
|
||||||
|
|
||||||
|
self.write(self._solar_conditions.to_json())
|
||||||
|
self.set_status(200)
|
||||||
|
self.set_header("Cache-Control", "no-store")
|
||||||
|
self.set_header("Content-Type", "application/json")
|
||||||
@@ -10,6 +10,7 @@ from server.handlers.api.addspot import APISpotHandler
|
|||||||
from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
||||||
from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler, APILookupGridHandler
|
from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler, APILookupGridHandler
|
||||||
from server.handlers.api.options import APIOptionsHandler
|
from server.handlers.api.options import APIOptionsHandler
|
||||||
|
from server.handlers.api.solar_conditions import APISolarConditionsHandler
|
||||||
from server.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
|
from server.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
|
||||||
from server.handlers.api.status import APIStatusHandler
|
from server.handlers.api.status import APIStatusHandler
|
||||||
from server.handlers.metrics import PrometheusMetricsHandler
|
from server.handlers.metrics import PrometheusMetricsHandler
|
||||||
@@ -19,11 +20,12 @@ from server.handlers.pagetemplate import PageTemplateHandler
|
|||||||
class WebServer:
|
class WebServer:
|
||||||
"""Provides the public-facing web server."""
|
"""Provides the public-facing web server."""
|
||||||
|
|
||||||
def __init__(self, spots, alerts, status_data, port):
|
def __init__(self, spots, alerts, solar_conditions, status_data, port):
|
||||||
"""Constructor"""
|
"""Constructor"""
|
||||||
|
|
||||||
self._spots = spots
|
self._spots = spots
|
||||||
self._alerts = alerts
|
self._alerts = alerts
|
||||||
|
self._solar_conditions = solar_conditions
|
||||||
self._sse_spot_queues = []
|
self._sse_spot_queues = []
|
||||||
self._sse_alert_queues = []
|
self._sse_alert_queues = []
|
||||||
self._status_data = status_data
|
self._status_data = status_data
|
||||||
@@ -59,6 +61,8 @@ class WebServer:
|
|||||||
{"sse_spot_queues": self._sse_spot_queues, "web_server_metrics": self.web_server_metrics}),
|
{"sse_spot_queues": self._sse_spot_queues, "web_server_metrics": self.web_server_metrics}),
|
||||||
(r"/api/v1/alerts/stream", APIAlertsStreamHandler,
|
(r"/api/v1/alerts/stream", APIAlertsStreamHandler,
|
||||||
{"sse_alert_queues": self._sse_alert_queues, "web_server_metrics": self.web_server_metrics}),
|
{"sse_alert_queues": self._sse_alert_queues, "web_server_metrics": self.web_server_metrics}),
|
||||||
|
(r"/api/v1/solar", APISolarConditionsHandler,
|
||||||
|
{"solar_conditions": self._solar_conditions, "web_server_metrics": self.web_server_metrics}),
|
||||||
(r"/api/v1/options", APIOptionsHandler,
|
(r"/api/v1/options", APIOptionsHandler,
|
||||||
{"status_data": self._status_data, "web_server_metrics": self.web_server_metrics}),
|
{"status_data": self._status_data, "web_server_metrics": self.web_server_metrics}),
|
||||||
(r"/api/v1/status", APIStatusHandler,
|
(r"/api/v1/status", APIStatusHandler,
|
||||||
|
|||||||
104
solarconditionsproviders/hamqsl.py
Normal file
104
solarconditionsproviders/hamqsl.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import logging
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from dateutil import parser as dateutil_parser, tz as dateutil_tz
|
||||||
|
|
||||||
|
from data.solar_conditions import HFBandCondition, VHFCondition
|
||||||
|
from solarconditionsproviders.http_solar_conditions_provider import HTTPSolarConditionsProvider
|
||||||
|
|
||||||
|
POLL_INTERVAL = 3600 # 1 hour
|
||||||
|
URL = "https://www.hamqsl.com/solarxml.php"
|
||||||
|
|
||||||
|
|
||||||
|
class HamQSL(HTTPSolarConditionsProvider):
|
||||||
|
"""Solar conditions provider using the HamQSL.com XML API (https://www.hamqsl.com/solarxml.php).
|
||||||
|
Provides solar flux index, geomagnetic indices, and HF/VHF propagation condition summaries."""
|
||||||
|
|
||||||
|
def __init__(self, provider_config):
|
||||||
|
super().__init__(provider_config, URL, POLL_INTERVAL)
|
||||||
|
|
||||||
|
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)
|
||||||
|
sd = root.find("solardata")
|
||||||
|
if sd is None:
|
||||||
|
logging.warning("HamQSL solar conditions API returned unexpected XML structure")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Some error checking functions in case the data is janky.
|
||||||
|
|
||||||
|
def text(tag, default=None):
|
||||||
|
el = sd.find(tag)
|
||||||
|
return el.text.strip() if el is not None and el.text else default
|
||||||
|
|
||||||
|
def float_val(tag, default=None):
|
||||||
|
try:
|
||||||
|
return float(text(tag))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
def int_val(tag, default=None):
|
||||||
|
try:
|
||||||
|
return int(text(tag))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
# Process HF band conditions
|
||||||
|
hf_conditions = []
|
||||||
|
calc = sd.find("calculatedconditions")
|
||||||
|
if calc is not None:
|
||||||
|
for band_el in calc.findall("band"):
|
||||||
|
name = band_el.get("name")
|
||||||
|
time = band_el.get("time")
|
||||||
|
condition = band_el.text.strip() if band_el.text else None
|
||||||
|
if name and time and condition:
|
||||||
|
hf_conditions.append(HFBandCondition(band=name, time=time, condition=condition))
|
||||||
|
|
||||||
|
# Process VHF propagation conditions
|
||||||
|
vhf_conditions = []
|
||||||
|
vhf = sd.find("calculatedvhfconditions")
|
||||||
|
if vhf is not None:
|
||||||
|
for ph_el in vhf.findall("phenomenon"):
|
||||||
|
vhf_conditions.append(VHFCondition(
|
||||||
|
phenomenon=ph_el.get("name"),
|
||||||
|
location=ph_el.get("location"),
|
||||||
|
condition=ph_el.text.strip() if ph_el.text else None
|
||||||
|
))
|
||||||
|
|
||||||
|
# Parse the "updated" timestamp string (format: "28 Mar 2026 0949 GMT") to UTC epoch seconds.
|
||||||
|
updated = None
|
||||||
|
updated_str = text("updated")
|
||||||
|
if updated_str:
|
||||||
|
try:
|
||||||
|
tz_abbr = updated_str.split()[-1]
|
||||||
|
timezone = dateutil_tz.gettz(tz_abbr)
|
||||||
|
if timezone is None:
|
||||||
|
raise ValueError("Unknown timezone abbreviation: " + tz_abbr)
|
||||||
|
dt = dateutil_parser.parse(updated_str, tzinfos={tz_abbr: timezone})
|
||||||
|
updated = dt.astimezone(pytz.UTC).timestamp()
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
logging.warning("HamQSL solar conditions API returned unrecognised timestamp format: " + updated_str)
|
||||||
|
|
||||||
|
# Return the data ready to be put into the solar conditions object.
|
||||||
|
return {
|
||||||
|
"updated": updated,
|
||||||
|
"sfi": int_val("solarflux"),
|
||||||
|
"a_index": int_val("aindex"),
|
||||||
|
"k_index": int_val("kindex"),
|
||||||
|
"x_ray": text("xray"),
|
||||||
|
"sunspots": int_val("sunspots"),
|
||||||
|
"proton_flux": int_val("protonflux"),
|
||||||
|
"electron_flux": int_val("electonflux"),
|
||||||
|
"aurora": int_val("aurora"),
|
||||||
|
"aurora_latitude": float_val("latdegree"),
|
||||||
|
"solar_wind": float_val("solarwind"),
|
||||||
|
"magnetic_field": float_val("magneticfield"),
|
||||||
|
"geomag_field": text("geomagfield"),
|
||||||
|
"geomag_noise": text("signalnoise"),
|
||||||
|
"hf_conditions": hf_conditions,
|
||||||
|
"vhf_conditions": vhf_conditions
|
||||||
|
}
|
||||||
61
solarconditionsproviders/http_solar_conditions_provider.py
Normal file
61
solarconditionsproviders/http_solar_conditions_provider.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from threading import Thread, Event
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from core.constants import HTTP_HEADERS
|
||||||
|
from solarconditionsproviders.solar_conditions_provider import SolarConditionsProvider
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPSolarConditionsProvider(SolarConditionsProvider):
|
||||||
|
"""Generic solar conditions provider for providers that request data via HTTP(S). Subclasses implement
|
||||||
|
_http_response_to_solar_conditions() to parse the specific API response format."""
|
||||||
|
|
||||||
|
def __init__(self, provider_config, url, poll_interval):
|
||||||
|
super().__init__(provider_config)
|
||||||
|
self._url = url
|
||||||
|
self._poll_interval = poll_interval
|
||||||
|
self._thread = None
|
||||||
|
self._stop_event = Event()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
logging.info("Set up query of " + self.name + " solar conditions API every " + str(self._poll_interval) + " seconds.")
|
||||||
|
self._thread = Thread(target=self._run, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
while True:
|
||||||
|
self._poll()
|
||||||
|
if self._stop_event.wait(timeout=self._poll_interval):
|
||||||
|
break
|
||||||
|
|
||||||
|
def _poll(self):
|
||||||
|
try:
|
||||||
|
logging.debug("Polling " + self.name + " solar conditions API...")
|
||||||
|
http_response = requests.get(self._url, headers=HTTP_HEADERS)
|
||||||
|
new_data = self._http_response_to_solar_conditions(http_response)
|
||||||
|
if new_data:
|
||||||
|
for key, value in new_data.items():
|
||||||
|
if hasattr(self._solar_conditions, key):
|
||||||
|
setattr(self._solar_conditions, key, value)
|
||||||
|
|
||||||
|
self.status = "OK"
|
||||||
|
self.last_update_time = datetime.now(pytz.UTC)
|
||||||
|
logging.debug("Received data from " + self.name + " solar conditions API.")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.status = "Error"
|
||||||
|
logging.exception("Exception in HTTP Solar Conditions Provider (" + self.name + ")")
|
||||||
|
self._stop_event.wait(timeout=1)
|
||||||
|
|
||||||
|
def _http_response_to_solar_conditions(self, http_response):
|
||||||
|
"""Convert an HTTP response into solar conditions data. Returns a dict mapping SolarConditions field
|
||||||
|
names to their new values, or None if the response could not be parsed. Only the fields returned will
|
||||||
|
be updated on the shared SolarConditions object; any fields not included will be left unchanged."""
|
||||||
|
|
||||||
|
raise NotImplementedError("Subclasses must implement this method")
|
||||||
32
solarconditionsproviders/solar_conditions_provider.py
Normal file
32
solarconditionsproviders/solar_conditions_provider.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
|
||||||
|
class SolarConditionsProvider:
|
||||||
|
"""Generic solar conditions provider class. Subclasses of this query individual APIs for space weather and
|
||||||
|
propagation data."""
|
||||||
|
|
||||||
|
def __init__(self, provider_config):
|
||||||
|
"""Constructor"""
|
||||||
|
|
||||||
|
self.name = provider_config["name"]
|
||||||
|
self.enabled = provider_config["enabled"]
|
||||||
|
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||||
|
self.status = "Not Started" if self.enabled else "Disabled"
|
||||||
|
self._solar_conditions = None
|
||||||
|
|
||||||
|
def setup(self, solar_conditions):
|
||||||
|
"""Set up the provider, giving it the solar conditions dict to update"""
|
||||||
|
|
||||||
|
self._solar_conditions = solar_conditions
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the provider. This should return immediately after spawning threads to access the remote resources"""
|
||||||
|
|
||||||
|
raise NotImplementedError("Subclasses must implement this method")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop any threads and prepare for application shutdown"""
|
||||||
|
|
||||||
|
raise NotImplementedError("Subclasses must implement this method")
|
||||||
27
spothole.py
27
spothole.py
@@ -8,6 +8,7 @@ import sys
|
|||||||
from diskcache import Cache
|
from diskcache import Cache
|
||||||
|
|
||||||
from core.cleanup import CleanupTimer
|
from core.cleanup import CleanupTimer
|
||||||
|
from data.solar_conditions import SolarConditions
|
||||||
from core.config import config, WEB_SERVER_PORT, SERVER_OWNER_CALLSIGN
|
from core.config import config, WEB_SERVER_PORT, SERVER_OWNER_CALLSIGN
|
||||||
from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION
|
from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION
|
||||||
from core.lookup_helper import lookup_helper
|
from core.lookup_helper import lookup_helper
|
||||||
@@ -17,10 +18,12 @@ from server.webserver import WebServer
|
|||||||
# Globals
|
# Globals
|
||||||
spots = Cache('cache/spots_cache')
|
spots = Cache('cache/spots_cache')
|
||||||
alerts = Cache('cache/alerts_cache')
|
alerts = Cache('cache/alerts_cache')
|
||||||
|
solar_conditions = SolarConditions()
|
||||||
web_server = None
|
web_server = None
|
||||||
status_data = {}
|
status_data = {}
|
||||||
spot_providers = []
|
spot_providers = []
|
||||||
alert_providers = []
|
alert_providers = []
|
||||||
|
solar_condition_providers = []
|
||||||
cleanup_timer = None
|
cleanup_timer = None
|
||||||
run = True
|
run = True
|
||||||
|
|
||||||
@@ -38,6 +41,9 @@ def shutdown(sig, frame):
|
|||||||
for ap in alert_providers:
|
for ap in alert_providers:
|
||||||
if ap.enabled:
|
if ap.enabled:
|
||||||
ap.stop()
|
ap.stop()
|
||||||
|
for scp in solar_condition_providers:
|
||||||
|
if scp.enabled:
|
||||||
|
scp.stop()
|
||||||
cleanup_timer.stop()
|
cleanup_timer.stop()
|
||||||
lookup_helper.stop()
|
lookup_helper.stop()
|
||||||
spots.close()
|
spots.close()
|
||||||
@@ -61,6 +67,14 @@ def get_alert_provider_from_config(config_providers_entry):
|
|||||||
return provider_class(config_providers_entry)
|
return provider_class(config_providers_entry)
|
||||||
|
|
||||||
|
|
||||||
|
def get_solar_conditions_provider_from_config(config_providers_entry):
|
||||||
|
"""Utility method to get a solar conditions provider based on the class specified in its config entry."""
|
||||||
|
|
||||||
|
module = importlib.import_module('solarconditionsproviders.' + config_providers_entry["class"].lower())
|
||||||
|
provider_class = getattr(module, config_providers_entry["class"])
|
||||||
|
return provider_class(config_providers_entry)
|
||||||
|
|
||||||
|
|
||||||
# Main function
|
# Main function
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Set up logging
|
# Set up logging
|
||||||
@@ -83,7 +97,7 @@ if __name__ == '__main__':
|
|||||||
lookup_helper.start()
|
lookup_helper.start()
|
||||||
|
|
||||||
# Set up web server
|
# Set up web server
|
||||||
web_server = WebServer(spots=spots, alerts=alerts, status_data=status_data, port=WEB_SERVER_PORT)
|
web_server = WebServer(spots=spots, alerts=alerts, solar_conditions=solar_conditions, status_data=status_data, port=WEB_SERVER_PORT)
|
||||||
|
|
||||||
# Fetch, set up and start spot providers
|
# Fetch, set up and start spot providers
|
||||||
for entry in config["spot-providers"]:
|
for entry in config["spot-providers"]:
|
||||||
@@ -101,6 +115,14 @@ if __name__ == '__main__':
|
|||||||
if p.enabled:
|
if p.enabled:
|
||||||
p.start()
|
p.start()
|
||||||
|
|
||||||
|
# Fetch, set up and start solar conditions providers
|
||||||
|
for entry in config.get("solar-condition-providers", []):
|
||||||
|
solar_condition_providers.append(get_solar_conditions_provider_from_config(entry))
|
||||||
|
for p in solar_condition_providers:
|
||||||
|
p.setup(solar_conditions=solar_conditions)
|
||||||
|
if p.enabled:
|
||||||
|
p.start()
|
||||||
|
|
||||||
# Set up timer to clear spot list of old data
|
# Set up timer to clear spot list of old data
|
||||||
cleanup_timer = CleanupTimer(spots=spots, alerts=alerts, web_server=web_server, cleanup_interval=60)
|
cleanup_timer = CleanupTimer(spots=spots, alerts=alerts, web_server=web_server, cleanup_interval=60)
|
||||||
cleanup_timer.start()
|
cleanup_timer.start()
|
||||||
@@ -108,7 +130,8 @@ if __name__ == '__main__':
|
|||||||
# Set up status reporter
|
# Set up status reporter
|
||||||
status_reporter = StatusReporter(status_data=status_data, spots=spots, alerts=alerts, web_server=web_server,
|
status_reporter = StatusReporter(status_data=status_data, spots=spots, alerts=alerts, web_server=web_server,
|
||||||
cleanup_timer=cleanup_timer, spot_providers=spot_providers,
|
cleanup_timer=cleanup_timer, spot_providers=spot_providers,
|
||||||
alert_providers=alert_providers, run_interval=5)
|
alert_providers=alert_providers,
|
||||||
|
solar_condition_providers=solar_condition_providers, run_interval=5)
|
||||||
status_reporter.start()
|
status_reporter.start()
|
||||||
|
|
||||||
logging.info("Startup complete.")
|
logging.info("Startup complete.")
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
<h4 class="mt-4">What data sources are supported?</h4>
|
<h4 class="mt-4">What data sources are supported?</h4>
|
||||||
<p>Spothole can retrieve spots from: <a href="https://www.dxcluster.info/telnet/">Telnet-based DX clusters</a>, the <a href="https://www.reversebeacon.net/">Reverse Beacon Network (RBN)</a>, the <a href="https://www.aprs-is.net/">APRS Internet Service (APRS-IS)</a>, <a href="https://pota.app">POTA</a>, <a href="https://www.sota.org.uk/">SOTA</a>, <a href="https://wwff.co/">WWFF</a>, <a href="https://www.cqgma.org/">GMA</a>, <a href="https://wwbota.net/">WWBOTA</a>, <a href="http://www.hema.org.uk/">HEMA</a>, <a href="https://www.parksnpeaks.org/">Parks 'n' Peaks</a>, <a href="https://ontheair.nz">ZLOTA</a>, <a href="https://www.wota.org.uk/">WOTA</a>, <a href="https://llota.app">LLOTA</a>, <a href="https://wwtota.com">WWTOTA</a>, the <a href="https://ukpacketradio.network/">UK Packet Repeater Network</a>, and any site based on the <a href="https://github.com/nischu/xOTA">xOTA software by nischu</a>.</p>
|
<p>Spothole can retrieve spots from: <a href="https://www.dxcluster.info/telnet/">Telnet-based DX clusters</a>, the <a href="https://www.reversebeacon.net/">Reverse Beacon Network (RBN)</a>, the <a href="https://www.aprs-is.net/">APRS Internet Service (APRS-IS)</a>, <a href="https://pota.app">POTA</a>, <a href="https://www.sota.org.uk/">SOTA</a>, <a href="https://wwff.co/">WWFF</a>, <a href="https://www.cqgma.org/">GMA</a>, <a href="https://wwbota.net/">WWBOTA</a>, <a href="http://www.hema.org.uk/">HEMA</a>, <a href="https://www.parksnpeaks.org/">Parks 'n' Peaks</a>, <a href="https://ontheair.nz">ZLOTA</a>, <a href="https://www.wota.org.uk/">WOTA</a>, <a href="https://llota.app">LLOTA</a>, <a href="https://wwtota.com">WWTOTA</a>, the <a href="https://ukpacketradio.network/">UK Packet Repeater Network</a>, and any site based on the <a href="https://github.com/nischu/xOTA">xOTA software by nischu</a>.</p>
|
||||||
<p>Spothole can retrieve alerts from: <a href="https://www.ng3k.com/">NG3K</a>, <a href="https://pota.app">POTA</a>, <a href="https://www.sota.org.uk/">SOTA</a>, <a href="https://wwff.co/">WWFF</a>, <a href="https://www.parksnpeaks.org/">Parks 'n' Peaks</a>, <a href="https://www.wota.org.uk/">WOTA</a> and <a href="https://www.beachesontheair.com/">BOTA</a>.</p>
|
<p>Spothole can retrieve alerts from: <a href="https://www.ng3k.com/">NG3K</a>, <a href="https://pota.app">POTA</a>, <a href="https://www.sota.org.uk/">SOTA</a>, <a href="https://wwff.co/">WWFF</a>, <a href="https://www.parksnpeaks.org/">Parks 'n' Peaks</a>, <a href="https://www.wota.org.uk/">WOTA</a> and <a href="https://www.beachesontheair.com/">BOTA</a>.</p>
|
||||||
|
<p>Spothole can retrieve solar and propagation condition data from <a href="https://www.hamqsl.com">HamQSL</a>.</p>
|
||||||
<p>Note that the server owner has not necessarily enabled all these data sources. In particular it is common to disable RBN, to avoid the server being swamped with FT8 traffic, and to disable APRS-IS and UK Packet Net so that the server only displays stations where there is likely to be an operator physically present for a QSO.</p>
|
<p>Note that the server owner has not necessarily enabled all these data sources. In particular it is common to disable RBN, to avoid the server being swamped with FT8 traffic, and to disable APRS-IS and UK Packet Net so that the server only displays stations where there is likely to be an operator physically present for a QSO.</p>
|
||||||
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: Parks on the Air (POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air (MOTA), the Amateur Radio Lighthouse Socirty (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National Parks Award (KRMNPA), Wainwrights on the Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air (WWTOTA), Worked All Britain (WAB), Worked All Ireland (WAI), and Toilets on the Air (TOTA).</p>
|
<p>Between the various data sources, the following Special Interest Groups (SIGs) are supported: Parks on the Air (POTA), Summits on the Air (SOTA), Worldwide Flora & Fauna (WWFF), Global Mountain Activity (GMA), Worldwide Bunkers on the Air (WWBOTA), HuMPs Excluding Marilyns Award (HEMA), Islands on the Air (IOTA), Mills on the Air (MOTA), the Amateur Radio Lighthouse Socirty (ARLHS), International Lighthouse Lightship Weekend (ILLW), Silos on the Air (SIOTA), World Castles Award (WCA), New Zealand on the Air (ZLOTA), Keith Roget Memorial National Parks Award (KRMNPA), Wainwrights on the Air (WOTA), Beaches on the Air (BOTA), Lagos y Lagunas On the Air (LLOTA), Towers on the Air (WWTOTA), Worked All Britain (WAB), Worked All Ireland (WAI), and Toilets on the Air (TOTA).</p>
|
||||||
<p>As of the time of writing in November 2025, I think Spothole captures essentially all outdoor radio programmes that have a defined reference list, and almost certainly those that have a spotting/alerting API. If you know of one I've missed, please let me know!</p>
|
<p>As of the time of writing in November 2025, I think Spothole captures essentially all outdoor radio programmes that have a defined reference list, and almost certainly those that have a spotting/alerting API. If you know of one I've missed, please let me know!</p>
|
||||||
@@ -61,12 +62,12 @@
|
|||||||
{% end %}
|
{% end %}
|
||||||
<p>Spothole is open source, so you can audit <a href="https://git.ianrenton.com/ian/spothole">the code</a> if you like.</p>
|
<p>Spothole is open source, so you can audit <a href="https://git.ianrenton.com/ian/spothole">the code</a> if you like.</p>
|
||||||
<h2 class="mt-4">Thanks</h2>
|
<h2 class="mt-4">Thanks</h2>
|
||||||
<p>This project would not have been possible without those volunteers who have taken it upon themselves to run DX clusters, xOTA programmes, DXpedition lists, callsign lookup databases, and other online tools on which Spothole's data is based.</p>
|
<p>This project would not have been possible without those volunteers who have taken it upon themselves to run DX clusters, xOTA programmes, DXpedition lists, callsign lookup databases, solar conditions and propagation modelling software, and other online tools on which Spothole's data is based. The vast majority of these are not profit-seeking and are made purely for the love of the hobby and to help others in the community. Spothole is standing on the shoulders of giants, who deserve a huge amount of thanks for all the work they put in.</p>
|
||||||
<p>Spothole is also dependent on a number of Python libraries, in particular pyhamtools, and many JavaScript libraries, as well as the Font Awesome icon set and flag icons from the Noto Color Emoji set, and MIT-licenced GeoJSON files for CQ and ITU zones from HA8TKS.</p>
|
<p>Spothole is also dependent on a number of Python libraries, in particular pyhamtools, and many JavaScript libraries, as well as the Font Awesome icon set and flag icons from the Noto Color Emoji set, and MIT-licenced GeoJSON files for CQ and ITU zones from HA8TKS.</p>
|
||||||
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
<p>This software is dedicated to the memory of Tom G1PJB, SK, a friend and colleague who sadly passed away around the time I started writing it in Autumn 2025. I was looking forward to showing it to you when it was done.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1774598773"></script>
|
<script src="/js/common.js?v=1774692270"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1774598773"></script>
|
<script src="/js/common.js?v=1774692270"></script>
|
||||||
<script src="/js/add-spot.js?v=1774598773"></script>
|
<script src="/js/add-spot.js?v=1774692270"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -56,8 +56,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1774598773"></script>
|
<script src="/js/common.js?v=1774692270"></script>
|
||||||
<script src="/js/alerts.js?v=1774598773"></script>
|
<script src="/js/alerts.js?v=1774692270"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
<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/common.js?v=1774598773"></script>
|
<script src="/js/common.js?v=1774692270"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1774598773"></script>
|
<script src="/js/spotsbandsandmap.js?v=1774692270"></script>
|
||||||
<script src="/js/bands.js?v=1774598773"></script>
|
<script src="/js/bands.js?v=1774692270"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -46,10 +46,10 @@
|
|||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
|
||||||
|
|
||||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1774598773"></script>
|
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1774692269"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1774598773"></script>
|
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1774692269"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1774598773"></script>
|
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1774692269"></script>
|
||||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1774598773"></script>
|
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1774692269"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -70,9 +70,9 @@
|
|||||||
<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/common.js?v=1774598773"></script>
|
<script src="/js/common.js?v=1774692270"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1774598773"></script>
|
<script src="/js/spotsbandsandmap.js?v=1774692270"></script>
|
||||||
<script src="/js/map.js?v=1774598773"></script>
|
<script src="/js/map.js?v=1774692270"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -87,9 +87,9 @@
|
|||||||
<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/common.js?v=1774598773"></script>
|
<script src="/js/common.js?v=1774692269"></script>
|
||||||
<script src="/js/spotsbandsandmap.js?v=1774598773"></script>
|
<script src="/js/spotsbandsandmap.js?v=1774692269"></script>
|
||||||
<script src="/js/spots.js?v=1774598773"></script>
|
<script src="/js/spots.js?v=1774692269"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
<div id="status-container" class="row row-cols-1 row-cols-md-4 g-4 mt-4"></div>
|
<div id="status-container" class="row row-cols-1 row-cols-md-4 g-4 mt-4"></div>
|
||||||
|
|
||||||
<script src="/js/common.js?v=1774598773"></script>
|
<script src="/js/common.js?v=1774692270"></script>
|
||||||
<script src="/js/status.js?v=1774598773"></script>
|
<script src="/js/status.js?v=1774692270"></script>
|
||||||
<script>$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --></script>
|
<script>$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||||
|
|
||||||
{% end %}
|
{% end %}
|
||||||
@@ -13,8 +13,13 @@ info:
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 1.2
|
||||||
|
|
||||||
|
* Added `/solar` endpoint for solar and propagation conditions.
|
||||||
|
* Added `solar_condition_providers` array to the `/status` response.
|
||||||
|
|
||||||
### 1.1
|
### 1.1
|
||||||
|
|
||||||
* Added Server-Sent Event API endpoints for spots and alerts.
|
* Added Server-Sent Event API endpoints for spots and alerts.
|
||||||
* Removed band colour and icon information from spots.
|
* Removed band colour and icon information from spots.
|
||||||
* Moved activation_score from top-level in Spot and Alert to be part of the SIGRef
|
* Moved activation_score from top-level in Spot and Alert to be part of the SIGRef
|
||||||
@@ -23,7 +28,7 @@ info:
|
|||||||
license:
|
license:
|
||||||
name: The Unlicense
|
name: The Unlicense
|
||||||
url: https://unlicense.org/#the-unlicense
|
url: https://unlicense.org/#the-unlicense
|
||||||
version: v1.1
|
version: v1.2
|
||||||
servers:
|
servers:
|
||||||
- url: https://spothole.app/api/v1
|
- url: https://spothole.app/api/v1
|
||||||
paths:
|
paths:
|
||||||
@@ -476,6 +481,11 @@ paths:
|
|||||||
description: An array of all the alert providers.
|
description: An array of all the alert providers.
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/AlertProviderStatus'
|
$ref: '#/components/schemas/AlertProviderStatus'
|
||||||
|
solar_condition_providers:
|
||||||
|
type: array
|
||||||
|
description: An array of all the solar conditions providers.
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/SolarConditionsProviderStatus'
|
||||||
|
|
||||||
|
|
||||||
/options:
|
/options:
|
||||||
@@ -772,6 +782,22 @@ paths:
|
|||||||
example: "Failed"
|
example: "Failed"
|
||||||
|
|
||||||
|
|
||||||
|
/solar:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- General
|
||||||
|
summary: Get solar and propagation conditions
|
||||||
|
description: Returns the current solar conditions and HF/VHF propagation condition summaries. This data is sourced from external providers (e.g. HamQSL) and updated periodically. All fields may be null if no provider has successfully fetched data yet.
|
||||||
|
operationId: solar
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Success
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SolarConditions'
|
||||||
|
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
Source:
|
Source:
|
||||||
@@ -1328,4 +1354,136 @@ components:
|
|||||||
ref_regex:
|
ref_regex:
|
||||||
type: string
|
type: string
|
||||||
description: Regex that matches this SIG's reference IDs. Generally for Spothole's own internal use, clients probably won't need this.
|
description: Regex that matches this SIG's reference IDs. Generally for Spothole's own internal use, clients probably won't need this.
|
||||||
example: "[A-Z]{2}\\-\\d+"
|
example: "[A-Z]{2}\\-\\d+"
|
||||||
|
|
||||||
|
HFBandCondition:
|
||||||
|
type: object
|
||||||
|
description: HF propagation conditions for a group of bands at a particular time of day.
|
||||||
|
properties:
|
||||||
|
band:
|
||||||
|
type: string
|
||||||
|
description: Band group name as used by the data source, e.g. "80m-40m", "30m-20m", "17m-15m", "10m-6m".
|
||||||
|
example: "80m-40m"
|
||||||
|
time:
|
||||||
|
type: string
|
||||||
|
description: Time of day these conditions apply to.
|
||||||
|
enum:
|
||||||
|
- day
|
||||||
|
- night
|
||||||
|
example: day
|
||||||
|
condition:
|
||||||
|
type: string
|
||||||
|
description: Propagation condition assessment.
|
||||||
|
enum:
|
||||||
|
- Good
|
||||||
|
- Fair
|
||||||
|
- Poor
|
||||||
|
example: Good
|
||||||
|
|
||||||
|
VHFCondition:
|
||||||
|
type: object
|
||||||
|
description: A VHF propagation phenomenon and its current condition.
|
||||||
|
properties:
|
||||||
|
phenomenon:
|
||||||
|
type: string
|
||||||
|
description: The name of the propagation phenomenon, e.g. "E-Skip", "Sporadic E".
|
||||||
|
example: "E-Skip"
|
||||||
|
location:
|
||||||
|
type: string
|
||||||
|
description: The geographic region this condition applies to, e.g. "Europe", "N America".
|
||||||
|
example: "Europe"
|
||||||
|
condition:
|
||||||
|
type: string
|
||||||
|
description: The current condition for this phenomenon and location.
|
||||||
|
example: "Band Closed"
|
||||||
|
|
||||||
|
SolarConditions:
|
||||||
|
type: object
|
||||||
|
description: Current solar and propagation conditions. All fields may be null if no provider has successfully fetched data yet.
|
||||||
|
properties:
|
||||||
|
updated:
|
||||||
|
type: number
|
||||||
|
description: Time that the data was last updated, UTC seconds since UNIX epoch
|
||||||
|
example: 1759579508
|
||||||
|
sfi:
|
||||||
|
type: integer
|
||||||
|
description: Solar Flux Index (SFI). Higher values generally indicate better HF propagation.
|
||||||
|
example: 170
|
||||||
|
a_index:
|
||||||
|
type: integer
|
||||||
|
description: A-index — daily geomagnetic activity index. Higher values indicate more disturbed conditions.
|
||||||
|
example: 7
|
||||||
|
k_index:
|
||||||
|
type: integer
|
||||||
|
description: K-index — 3-hour geomagnetic activity index, 0–9. Values of 5 or above indicate a geomagnetic storm.
|
||||||
|
example: 2
|
||||||
|
x_ray:
|
||||||
|
type: string
|
||||||
|
description: Current X-ray flux class, e.g. "B2.3", "C1.0", "M5.0".
|
||||||
|
example: "B2.3"
|
||||||
|
proton_flux:
|
||||||
|
type: integer
|
||||||
|
description: Proton flux level.
|
||||||
|
example: 1
|
||||||
|
electron_flux:
|
||||||
|
type: integer
|
||||||
|
description: Electron flux level.
|
||||||
|
example: 631
|
||||||
|
aurora:
|
||||||
|
type: integer
|
||||||
|
description: Aurora activity level.
|
||||||
|
example: 5
|
||||||
|
aurora_latitude:
|
||||||
|
type: number
|
||||||
|
description: Latitude in degrees of the equatorward boundary of the aurora.
|
||||||
|
example: 66.3
|
||||||
|
sunspots:
|
||||||
|
type: integer
|
||||||
|
description: Current sunspot count.
|
||||||
|
example: 87
|
||||||
|
solar_wind:
|
||||||
|
type: number
|
||||||
|
description: Solar wind speed in km/s.
|
||||||
|
example: 356.6
|
||||||
|
magnetic_field:
|
||||||
|
type: number
|
||||||
|
description: Interplanetary magnetic field (IMF) strength in nT.
|
||||||
|
example: 2.5
|
||||||
|
geomag_field:
|
||||||
|
type: string
|
||||||
|
description: Geomagnetic field condition summary.
|
||||||
|
example: "Active"
|
||||||
|
geomag_noise:
|
||||||
|
type: string
|
||||||
|
description: Geomagnetic background noise level on HF, using S-units.
|
||||||
|
example: "S0"
|
||||||
|
hf_conditions:
|
||||||
|
type: array
|
||||||
|
description: HF propagation condition assessments by band group and time of day.
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/HFBandCondition'
|
||||||
|
vhf_conditions:
|
||||||
|
type: array
|
||||||
|
description: VHF propagation condition assessments by phenomenon and location.
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/VHFCondition'
|
||||||
|
|
||||||
|
SolarConditionsProviderStatus:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: The name of the provider.
|
||||||
|
example: HamQSL
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: Whether the provider is enabled or not.
|
||||||
|
example: true
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: The status of the provider.
|
||||||
|
example: OK
|
||||||
|
last_updated:
|
||||||
|
type: number
|
||||||
|
description: The last time at which this provider received data, UTC seconds since UNIX epoch. If this is zero, the provider has never updated.
|
||||||
|
example: 1759579508
|
||||||
Reference in New Issue
Block a user