Add descriptions for solar conditions #92

This commit is contained in:
Ian Renton
2026-03-28 10:39:26 +00:00
parent 1173af6a9d
commit 2a5e0db5bc
14 changed files with 203 additions and 49 deletions

View File

@@ -1,6 +1,87 @@
import json
from dataclasses import dataclass
# Lookup tables for derived text descriptions.
# Each threshold-based table is a list of (min_value, description) pairs in descending order;
# the first entry whose threshold the value meets or exceeds is used.
BLACKOUT_DESCRIPTIONS = {
"X": "Extreme HF radio blackout on entire sunlit side",
"M": "Wide area HF radio blackout on sunlit side",
"C": "Occasional loss of HF communications on sunlit side",
"B": "No significant radio blackout",
"A": "No impact",
}
PROTON_FLUX_DESCRIPTIONS = [
(1000000, "Complete HF blackout in polar regions"),
(100000, "Partial HF blackout in polar regions"),
(10000, "Degraded HF propagation in polar regions"),
(1000, "Small effect on HF propagation in polar regions"),
(100, "Minor effect on HF propagation in polar regions"),
(10, "Very minor effect on HF propagation in polar regions"),
(0, "No impact"),
]
SOLAR_STORM_SCALES = [
(100000, 5),
(10000, 4),
(1000, 3),
(100, 2),
(10, 1),
(0, 0),
]
GEOMAG_STORM_DESCRIPTIONS = [
(9, "Solar storm. Complete HF blackout, S30+ noise"),
(8, "Solar storm. HF sporadic only, S20-30 noise"),
(7, "Solar storm. HF intermittent, S9-20 noise"),
(6, "Solar storm. HF fading at higher latitudes, S6-9 noise"),
(5, "Solar storm. HF fading at higher latitudes, S4-6 noise"),
(4, "Active. Minor HF fading at higher latitudes, S2-3 noise"),
(3, "Unsettled. Minor HF fading at higher latitudes, S2-3 noise"),
(2, "Inactive. No impact, S0-2 noise"),
(1, "Quiet. No impact, S0-2 noise"),
(0, "Quiet. No impact, S0-2 noise"),
]
GEOMAG_STORM_SCALES = [
(9, 5),
(8, 4),
(7, 3),
(6, 2),
(5, 1),
(0, 0),
]
BAND_CONDITIONS_DESCRIPTIONS = [
(200, "Reliable conditions on all bands including 6m"),
(150, "Excellent conditions on all bands up to 10m, occasional 6m openings"),
(120, "Fair to good conditions on all bands up to 10m"),
(90, "Fair conditions on bands up to 15m"),
(70, "Poor to fair conditions on bands up to 20m"),
(0, "Bands above 40m unusable"),
]
ELECTRON_FLUX_DESCRIPTIONS = [
(1000, "Partial to complete HF blackout in polar regions"),
(100, "Degraded HF propagation in polar regions"),
(10, "Minor impact on HF in polar regions"),
(0, "No impact"),
]
def _lookup_by_threshold(value, table, default=None):
"""Return the description from a threshold table for the given numeric value.
The table is a list of (min_value, description) pairs in descending order."""
if value is None:
return default
for threshold, description in table:
if value >= threshold:
return description
return default
@dataclass
class HFBandCondition:
@@ -63,6 +144,36 @@ class SolarConditions:
# VHF propagation phenomena
vhf_conditions: list = None # list[VHFCondition]
# Derived values (populated by infer_descriptions())
# HF radio blackout risk, derived from x_ray
blackout_desc: str = None
# Solar radiation storm level description, derived from proton_flux
proton_flux_desc: str = None
# Solar radiation storm scale number (S0S5), derived from proton_flux
solar_storm_scale: int = None
# Geomagnetic storm level description, derived from k_index
geomag_storm_desc: str = None
# Geomagnetic storm scale number (G0G5), derived from k_index
geomag_storm_scale: int = None
# Overall HF band conditions summary, derived from sfi
band_conditions_desc: str = None
# Electron flux level, derived from electron_flux
electron_flux_desc: str = None
def infer_descriptions(self):
"""Populate derived text description fields from the current numeric/raw field values."""
# blackout_desc: use the X-ray flux class letter (first character of x_ray)
if self.x_ray and len(self.x_ray) > 0:
self.blackout_desc = BLACKOUT_DESCRIPTIONS.get(self.x_ray[0].upper())
self.proton_flux_desc = _lookup_by_threshold(self.proton_flux, PROTON_FLUX_DESCRIPTIONS)
self.solar_storm_scale = _lookup_by_threshold(self.proton_flux, SOLAR_STORM_SCALES)
self.geomag_storm_desc = _lookup_by_threshold(self.k_index, GEOMAG_STORM_DESCRIPTIONS)
self.geomag_storm_scale = _lookup_by_threshold(self.k_index, GEOMAG_STORM_SCALES)
self.band_conditions_desc = _lookup_by_threshold(self.sfi, BAND_CONDITIONS_DESCRIPTIONS)
self.electron_flux_desc = _lookup_by_threshold(self.electron_flux, ELECTRON_FLUX_DESCRIPTIONS)
def to_json(self):
"""JSON serialise"""

View File

@@ -21,7 +21,8 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
self._stop_event = Event()
def start(self):
logging.info("Set up query of " + self.name + " solar conditions API every " + str(self._poll_interval) + " seconds.")
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()
@@ -39,10 +40,7 @@ class HTTPSolarConditionsProvider(SolarConditionsProvider):
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.update_data(new_data)
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)

View File

@@ -30,3 +30,12 @@ class SolarConditionsProvider:
"""Stop any threads and prepare for application shutdown"""
raise NotImplementedError("Subclasses must implement this method")
def update_data(self, new_data):
"""Update the solar conditions object with new data"""
if new_data:
for key, value in new_data.items():
if hasattr(self._solar_conditions, key):
setattr(self._solar_conditions, key, value)
self._solar_conditions.infer_descriptions()

View File

@@ -67,7 +67,7 @@
<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>
<script src="/js/common.js?v=1774692270"></script>
<script src="/js/common.js?v=1774694366"></script>
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -69,8 +69,8 @@
</div>
<script src="/js/common.js?v=1774692270"></script>
<script src="/js/add-spot.js?v=1774692270"></script>
<script src="/js/common.js?v=1774694366"></script>
<script src="/js/add-spot.js?v=1774694366"></script>
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -56,8 +56,8 @@
</div>
<script src="/js/common.js?v=1774692270"></script>
<script src="/js/alerts.js?v=1774692270"></script>
<script src="/js/common.js?v=1774694367"></script>
<script src="/js/alerts.js?v=1774694367"></script>
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -62,9 +62,9 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/common.js?v=1774692270"></script>
<script src="/js/spotsbandsandmap.js?v=1774692270"></script>
<script src="/js/bands.js?v=1774692270"></script>
<script src="/js/common.js?v=1774694366"></script>
<script src="/js/spotsbandsandmap.js?v=1774694366"></script>
<script src="/js/bands.js?v=1774694366"></script>
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -46,10 +46,10 @@
crossorigin="anonymous"></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=1774692269"></script>
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1774692269"></script>
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1774692269"></script>
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1774692269"></script>
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1774694366"></script>
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1774694366"></script>
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1774694366"></script>
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1774694366"></script>
</head>
<body>
@@ -67,7 +67,7 @@
<li class="nav-item ms-4"><a href="/" class="nav-link" id="nav-link-spots"><i class="fa-solid fa-tower-cell"></i> Spots</a></li>
<li class="nav-item ms-4"><a href="/map" class="nav-link" id="nav-link-map"><i class="fa-solid fa-map"></i> Map</a></li>
<li class="nav-item ms-4"><a href="/bands" class="nav-link" id="nav-link-bands"><i class="fa-solid fa-ruler-vertical"></i> Bands</a></li>
<li class="nav-item ms-4"><a href="/alerts" class="nav-link" id="nav-link-alerts"><i class="fa-solid fa-bell"></i> Alerts</a></li>
<li class="nav-item ms-4"><a href="/alerts" class="nav-link" id="nav-link-alerts"><i class="fa-solid fa-clock"></i> Upcoming</a></li>
{% if allow_spotting %}
<li class="nav-item ms-4"><a href="/add-spot" class="nav-link" id="nav-link-add-spot"><i class="fa-solid fa-comment"></i> Add&nbsp;Spot</a></li>
{% end %}

View File

@@ -70,9 +70,9 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/common.js?v=1774692270"></script>
<script src="/js/spotsbandsandmap.js?v=1774692270"></script>
<script src="/js/map.js?v=1774692270"></script>
<script src="/js/common.js?v=1774694367"></script>
<script src="/js/spotsbandsandmap.js?v=1774694367"></script>
<script src="/js/map.js?v=1774694367"></script>
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -87,9 +87,9 @@
<script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script>
<script src="/js/common.js?v=1774692269"></script>
<script src="/js/spotsbandsandmap.js?v=1774692269"></script>
<script src="/js/spots.js?v=1774692269"></script>
<script src="/js/common.js?v=1774694366"></script>
<script src="/js/spotsbandsandmap.js?v=1774694366"></script>
<script src="/js/spots.js?v=1774694366"></script>
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -3,8 +3,8 @@
<div id="status-container" class="row row-cols-1 row-cols-md-4 g-4 mt-4"></div>
<script src="/js/common.js?v=1774692270"></script>
<script src="/js/status.js?v=1774692270"></script>
<script src="/js/common.js?v=1774694366"></script>
<script src="/js/status.js?v=1774694366"></script>
<script>$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %}

View File

@@ -1,4 +1,5 @@
<div class="d-inline-flex gap-1">
<button id="filters-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="button" onclick="toggleFiltersPanel();"><i class="fa-solid fa-filter"></i>&nbsp;Filters</button>
<button id="display-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="button" onclick="toggleDisplayPanel();"><i class="fa-solid fa-desktop"></i>&nbsp;Display</button>
<button id="conditions-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="button" onclick="toggleConditionsPanel();"><i class="fa-solid fa-sun"></i><span class="hideonmobile">&nbsp;Conditions</span></button>
<button id="filters-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="button" onclick="toggleFiltersPanel();"><i class="fa-solid fa-filter"></i><span class="hideonmobile">&nbsp;Filters</span></button>
<button id="display-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="button" onclick="toggleDisplayPanel();"><i class="fa-solid fa-desktop"></i><span class="hideonmobile">&nbsp;Display</span></button>
</div>

View File

@@ -1362,18 +1362,18 @@ components:
properties:
band:
type: string
description: Band group name as used by the data source, e.g. "80m-40m", "30m-20m", "17m-15m", "10m-6m".
description: Band group, e.g. "80m-40m", "30m-20m", "17m-15m", "10m-6m". As provided by HamQSL.
example: "80m-40m"
time:
type: string
description: Time of day these conditions apply to.
description: Time of day these conditions apply to. As provided by HamQSL.
enum:
- day
- night
example: day
condition:
type: string
description: Propagation condition assessment.
description: Propagation condition assessment. As provided by HamQSL.
enum:
- Good
- Fair
@@ -1386,12 +1386,12 @@ components:
properties:
phenomenon:
type: string
description: The name of the propagation phenomenon, e.g. "E-Skip", "Sporadic E".
description: The name of the propagation phenomenon, e.g. "E-Skip", "vhf-aurora". As provided by HamQSL.
example: "E-Skip"
location:
type: string
description: The geographic region this condition applies to, e.g. "Europe", "N America".
example: "Europe"
description: The geographic region this condition applies to, e.g. "europe", "north_america", "northern_hemi". As provided by HamQSL.
example: "europe"
condition:
type: string
description: The current condition for this phenomenon and location.
@@ -1407,66 +1407,98 @@ components:
example: 1759579508
sfi:
type: integer
description: Solar Flux Index (SFI). Higher values generally indicate better HF propagation.
description: Solar Flux Index (SFI)
example: 170
a_index:
type: integer
description: A-index — daily geomagnetic activity index. Higher values indicate more disturbed conditions.
description: Daily geomagnetic activity index
example: 7
k_index:
type: integer
description: K-index — 3-hour geomagnetic activity index, 09. Values of 5 or above indicate a geomagnetic storm.
description: 3-hour geomagnetic activity index, 09
example: 2
x_ray:
type: string
description: Current X-ray flux class, e.g. "B2.3", "C1.0", "M5.0".
description: Current X-ray flux class
example: "B2.3"
proton_flux:
type: integer
description: Proton flux level.
description: Proton flux level
example: 1
electron_flux:
type: integer
description: Electron flux level.
description: Electron flux level
example: 631
aurora:
type: integer
description: Aurora activity level.
description: Aurora activity level
example: 5
aurora_latitude:
type: number
description: Latitude in degrees of the equatorward boundary of the aurora.
description: Lowest latitude at which aurora should be visible
example: 66.3
sunspots:
type: integer
description: Current sunspot count.
description: Sunspot count
example: 87
solar_wind:
type: number
description: Solar wind speed in km/s.
description: Solar wind speed in km/s
example: 356.6
magnetic_field:
type: number
description: Interplanetary magnetic field (IMF) strength in nT.
description: Interplanetary magnetic field strength in nT
example: 2.5
geomag_field:
type: string
description: Geomagnetic field condition summary.
description: Geomagnetic field condition summary
example: "Active"
geomag_noise:
type: string
description: Geomagnetic background noise level on HF, using S-units.
description: Geomagnetic background noise level on HF, in S-units
example: "S0"
hf_conditions:
type: array
description: HF propagation condition assessments by band group and time of day.
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.
description: VHF propagation condition assessments by phenomenon and location
items:
$ref: '#/components/schemas/VHFCondition'
blackout_desc:
type: string
description: HF radio blackout risk description, derived from the X-ray flux class.
example: "No significant radio blackout"
proton_flux_desc:
type: string
description: Solar radiation storm level description, derived from proton flux.
example: "No solar radiation storm"
solar_storm_scale:
type: integer
description: Solar radiation storm scale number (S0-S5), derived from proton flux. S0 = none, S5 = extreme.
minimum: 0
maximum: 5
example: 0
geomag_storm_desc:
type: string
description: Geomagnetic storm level description, derived from K-index.
example: "Quiet"
geomag_storm_scale:
type: integer
description: Geomagnetic storm scale number (G0-G5), derived from K-index. G0 = none, G5 = extreme.
minimum: 0
maximum: 5
example: 0
band_conditions_desc:
type: string
description: Overall HF band conditions summary, derived from Solar Flux Index.
example: "Fair to good conditions on all bands up to 10m"
electron_flux_desc:
type: string
description: Electron flux impact description, derived from electron flux level.
example: "No impact"
SolarConditionsProviderStatus:
type: object

View File

@@ -3,6 +3,9 @@
.navbar-nav .nav-link.active {
font-weight: bold;
}
.navbar-nav .nav-link i {
margin-right: 0.2em;
}
/* In embedded mode, hide header/footer/settings. "#header div" is kind of janky but for some reason if we hide the
whole of #header, the map vertical sizing breaks. */