From a7a45190cb82259489401c70b887a022318930c7 Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Sat, 16 May 2026 11:04:40 +0100 Subject: [PATCH] Make ionosonde_data a map keyed by URSI, and on polling the website, replace data for the specific URSI rather than overwriting everything. This allows us to preserve data from an older lookup if the website is down or returns nothing --- data/solar_conditions.py | 4 ++-- solarconditionsproviders/giroionosonde.py | 26 +++++++++++------------ templates/about.html | 2 +- templates/add_spot.html | 4 ++-- templates/alerts.html | 4 ++-- templates/bands.html | 6 +++--- templates/base.html | 8 +++---- templates/conditions.html | 4 ++-- templates/map.html | 6 +++--- templates/spots.html | 6 +++--- templates/status.html | 4 ++-- webassets/apidocs/openapi.yml | 12 +++++------ webassets/js/conditions.js | 11 +++++----- 13 files changed, 48 insertions(+), 49 deletions(-) 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($('