diff --git a/data/solar_conditions.py b/data/solar_conditions.py index 07c56a5..f3dc793 100644 --- a/data/solar_conditions.py +++ b/data/solar_conditions.py @@ -161,8 +161,8 @@ class SolarConditions: blackout_forecast_r1r2: dict = None # NOAA Radio Blackout (R3 or greater) probability forecast, keyed by UNIX timestamp of start of day UTC blackout_forecast_r3_or_greater: dict = None - # Ionosonde measurements from LGDC, list of dicts with keys: ursi, name, fof2, muf - ionosonde_data: list = None + # Ionosonde measurements from LGDC, dict keyed by URSI code, values are dicts with keys: ursi, name, fof2, muf + ionosonde_data: dict = None # Derived values (populated by infer_descriptions()) # HF radio blackout risk description, derived from xray diff --git a/solarconditionsproviders/giroionosonde.py b/solarconditionsproviders/giroionosonde.py index 5add621..b15c54d 100644 --- a/solarconditionsproviders/giroionosonde.py +++ b/solarconditionsproviders/giroionosonde.py @@ -51,34 +51,33 @@ class GIROIonosonde(SolarConditionsProvider): def _poll(self): try: - logging.debug(f"Polling {self.name} ionosonde data...") + logging.debug(f"Polling GIRO ionosonde data...") now = datetime.now(timezone.utc) from_time = now - timedelta(hours=HISTORY_HOURS) - results = [] + ionosonde_data = dict(self._solar_conditions.ionosonde_data or {}) + updated_count = 0 for station in self._stations: if self._stop_event.is_set(): break ursi = station["ursi"] name = station["name"] - entry = {"ursi": ursi, "name": name, "fof2": None, "muf": None} try: fof2, muf = self._fetch_station_data(ursi, from_time, now) - entry["fof2"] = fof2 - entry["muf"] = muf if fof2 and muf: - results.append(entry) + ionosonde_data[ursi] = {"ursi": ursi, "name": name, "fof2": fof2, "muf": muf} + updated_count += 1 except Exception: - logging.debug(f"Could not fetch ionosonde data for {ursi} ({name})") + logging.warning(f"Could not fetch ionosonde data for {ursi} ({name})") - self.update_data({"ionosonde_data": results}) + self.update_data({"ionosonde_data": ionosonde_data}) self.status = "OK" self.last_update_time = datetime.now(pytz.UTC) - logging.debug(f"Received ionosonde data for {len(results)} stations from {self.name}.") + logging.debug(f"Updated ionosonde data for {updated_count} stations.") except Exception: self.status = "Error" - logging.exception(f"Exception in GIRO Ionosonde data provider ({self.name})") + logging.exception(f"Exception in GIRO Ionosonde data provider") self._stop_event.wait(timeout=1) def _fetch_station_data(self, ursi, from_time, to_time): @@ -86,9 +85,7 @@ class GIROIonosonde(SolarConditionsProvider): from_str = from_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&DMUF=3000&fromDate={from_str}&toDate={to_str}" - ) + url = f"{LGDC_URL}?ursiCode={ursi}&charName=foF2,MUFD&DMUF=3000&fromDate={from_str}&toDate={to_str}" response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15)) if response.status_code != 200: return None, None @@ -104,10 +101,11 @@ class GIROIonosonde(SolarConditionsProvider): line = line.strip() if not line or line.startswith('#'): continue - # Data rows: timestamp CS foF2 QD MUFD QD + # Data rows have the following format: timestamp CS foF2 QD MUFD QD parts = line.split() if len(parts) >= 5: try: + # Python 3.8 TZ parsing fudge ts = datetime.fromisoformat(parts[0].replace('Z', '+00:00')).timestamp() except ValueError: continue diff --git a/templates/about.html b/templates/about.html index 4f95d0d..165ed88 100644 --- a/templates/about.html +++ b/templates/about.html @@ -69,7 +69,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 3c18416..95964dd 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 ed836f1..076c652 100644 --- a/templates/alerts.html +++ b/templates/alerts.html @@ -70,8 +70,8 @@ - - + + {% end %} \ No newline at end of file diff --git a/templates/bands.html b/templates/bands.html index eba8349..c56e993 100644 --- a/templates/bands.html +++ b/templates/bands.html @@ -76,9 +76,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index e4b9506..5a57e3e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,7 +24,7 @@ Spothole - + @@ -52,9 +52,9 @@ integrity="sha384-L1eE4eD41kpBIWe2I0eHy+GnEUC4RIpcvibVW2JCminuPlTl+2Bc528iPdVMg5Dn" crossorigin="anonymous"> - - - + + + diff --git a/templates/conditions.html b/templates/conditions.html index 1a55dda..69814ec 100644 --- a/templates/conditions.html +++ b/templates/conditions.html @@ -249,8 +249,8 @@ - - + + diff --git a/templates/map.html b/templates/map.html index 0e430bb..d7800fc 100644 --- a/templates/map.html +++ b/templates/map.html @@ -94,9 +94,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/spots.html b/templates/spots.html index 451b551..8af4444 100644 --- a/templates/spots.html +++ b/templates/spots.html @@ -104,9 +104,9 @@ - - - + + + {% end %} \ No newline at end of file diff --git a/templates/status.html b/templates/status.html index f2ad850..e2fc24e 100644 --- a/templates/status.html +++ b/templates/status.html @@ -59,8 +59,8 @@ - - + + diff --git a/webassets/apidocs/openapi.yml b/webassets/apidocs/openapi.yml index 4655e3f..2603407 100644 --- a/webassets/apidocs/openapi.yml +++ b/webassets/apidocs/openapi.yml @@ -15,7 +15,7 @@ info: ### 1.4 - * `/solar` response now includes `ionosonde_data`, a list of ionosonde station measurements (foF2 and MUF) sourced from the GIRO Data Center. + * `/solar` response now includes `ionosonde_data`, which contains ionosonde station measurements (foF2 and MUF) sourced from the GIRO Data Center. ### 1.3 @@ -1688,13 +1688,13 @@ components: description: Electron flux impact description, derived from electron flux level. example: "No impact" ionosonde_data: - type: array + type: object nullable: true description: > - Ionosonde measurements from the GIRO Data Center, covering active stations listed in the - system. Only stations for which data was successfully retrieved are included. Null if the - GIROIonosonde provider has not yet completed its first poll. - items: + Ionosonde measurements from the GIRO Data Center, keyed by URSI station code. Only + stations for which data was successfully retrieved are included. Null if the + GIROIonosonde provider has not yet completed its first poll or if this data source is disabled. + additionalProperties: $ref: '#/components/schemas/IonosondeStation' IonosondeStation: diff --git a/webassets/js/conditions.js b/webassets/js/conditions.js index f5ee08b..948c6ec 100644 --- a/webassets/js/conditions.js +++ b/webassets/js/conditions.js @@ -115,7 +115,7 @@ function loadSolarConditions() { // Ionosonde - if (jsonData.ionosonde_data && jsonData.ionosonde_data.length > 0) { + if (jsonData.ionosonde_data && Object.keys(jsonData.ionosonde_data).length > 0) { ionosondeData = jsonData.ionosonde_data; populateIonosondeDropdown(ionosondeData); renderIonosondeData(); @@ -366,9 +366,12 @@ function populateIonosondeDropdown(data) { const savedUrsi = localStorage.getItem('#ionosonde-station:value'); const savedValue = savedUrsi ? JSON.parse(savedUrsi) : null; select.empty(); - data.forEach(function (station) { + // Sort by station name rather than URSI because station name is what's displayed, and any out-of-order names might + // confuse the user + Object.values(data).sort((a, b) => a.name.localeCompare(b.name)).forEach(function (station) { select.append($('