diff --git a/data/solar_conditions.py b/data/solar_conditions.py index 0fa031d..bb5c3e7 100644 --- a/data/solar_conditions.py +++ b/data/solar_conditions.py @@ -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 (S0–S5), 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 (G0–G5), 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""" diff --git a/solarconditionsproviders/http_solar_conditions_provider.py b/solarconditionsproviders/http_solar_conditions_provider.py index 655c748..36c5af6 100644 --- a/solarconditionsproviders/http_solar_conditions_provider.py +++ b/solarconditionsproviders/http_solar_conditions_provider.py @@ -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) diff --git a/solarconditionsproviders/solar_conditions_provider.py b/solarconditionsproviders/solar_conditions_provider.py index f9a42be..364625b 100644 --- a/solarconditionsproviders/solar_conditions_provider.py +++ b/solarconditionsproviders/solar_conditions_provider.py @@ -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() diff --git a/templates/about.html b/templates/about.html index d387e3a..8923dd2 100644 --- a/templates/about.html +++ b/templates/about.html @@ -67,7 +67,7 @@
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.
- + {% end %} \ No newline at end of file diff --git a/templates/add_spot.html b/templates/add_spot.html index 3a59a04..5cf875b 100644 --- a/templates/add_spot.html +++ b/templates/add_spot.html @@ -69,8 +69,8 @@ - - + + {% end %} \ No newline at end of file diff --git a/templates/alerts.html b/templates/alerts.html index 101c429..ba3a25d 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -56,8 +56,8 @@ - - + + {% end %} \ No newline at end of file diff --git a/templates/bands.html b/templates/bands.html index 7c5e88f..4ee4211 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -62,9 +62,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 3812d93..fb06721 100644 --- a/templates/base.html +++ b/templates/base.html @@ -46,10 +46,10 @@ crossorigin="anonymous"> - - - - + + + + @@ -67,7 +67,7 @@