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 @@ - + {% if allow_spotting %} {% end %} diff --git a/templates/map.html b/templates/map.html index ccf2444..ccffb1f 100644 --- a/templates/map.html +++ b/templates/map.html @@ -70,9 +70,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/spots.html b/templates/spots.html index 1018e33..d198b5b 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -87,9 +87,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/status.html b/templates/status.html index b7a46d7..75c19d2 100644 --- a/templates/status.html +++ b/templates/status.html @@ -3,8 +3,8 @@
- - + + {% end %} \ No newline at end of file diff --git a/templates/widgets/filters-display-buttons.html b/templates/widgets/filters-display-buttons.html index f6cf612..e25bbca 100644 --- a/templates/widgets/filters-display-buttons.html +++ b/templates/widgets/filters-display-buttons.html @@ -1,4 +1,5 @@
- - + + +
\ No newline at end of file diff --git a/webassets/apidocs/openapi.yml b/webassets/apidocs/openapi.yml index 26d86ef..9b9d516 100644 --- a/webassets/apidocs/openapi.yml +++ b/webassets/apidocs/openapi.yml @@ -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, 0–9. Values of 5 or above indicate a geomagnetic storm. + description: 3-hour geomagnetic activity index, 0–9 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 diff --git a/webassets/css/style.css b/webassets/css/style.css index 5c87f8d..da7d1f8 100644 --- a/webassets/css/style.css +++ b/webassets/css/style.css @@ -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. */