mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-06-24 05:35:10 +00:00
Workaround to fetch ionosonde data from KC2G since the GIRO data source often seems to be down.
This commit is contained in:
121
solarconditionsproviders/kc2gprop.py
Normal file
121
solarconditionsproviders/kc2gprop.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from threading import Thread, Event
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
|
||||
from core.constants import HTTP_HEADERS
|
||||
from solarconditionsproviders.ionosonde_utils import compute_band_states
|
||||
from solarconditionsproviders.solar_conditions_provider import SolarConditionsProvider
|
||||
|
||||
POLL_INTERVAL = 900 # 15 minutes
|
||||
KC2G_URL = "https://prop.kc2g.com/api/stations.json"
|
||||
HISTORY_HOURS = 24
|
||||
|
||||
|
||||
class KC2GProp(SolarConditionsProvider):
|
||||
"""Solar conditions provider using ionosonde data from prop.kc2g.com. The API returns only the latest reading per
|
||||
station, so this provider polls every 15 minutes and accumulates a 24-hour time series by merging each new reading
|
||||
into the persisted ionosonde_data, producing the same data structure as GIROIonosonde.
|
||||
|
||||
Can run alongside GIROIonosonde: KC2G provides foF2/MUF with good reliability, while GIRO supplements with LUF
|
||||
readings. Stations from each source that the other does not cover are preserved."""
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(provider_config)
|
||||
self._thread = None
|
||||
self._stop_event = Event()
|
||||
|
||||
def start(self):
|
||||
logging.info(f"Set up query of KC2G ionosonde data API every {POLL_INTERVAL} seconds.")
|
||||
self._thread = Thread(target=self._run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._stop_event.set()
|
||||
|
||||
def _run(self):
|
||||
while True:
|
||||
self._poll()
|
||||
if self._stop_event.wait(timeout=POLL_INTERVAL):
|
||||
break
|
||||
|
||||
def _poll(self):
|
||||
try:
|
||||
logging.debug("Polling KC2G ionosonde data...")
|
||||
response = requests.get(KC2G_URL, headers=HTTP_HEADERS, timeout=(5, 30))
|
||||
if response.status_code != 200:
|
||||
logging.warning(f"KC2G ionosonde API returned HTTP {response.status_code}")
|
||||
return
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
cutoff_ts = (now - timedelta(hours=HISTORY_HOURS)).timestamp()
|
||||
|
||||
# Start from existing ionosonde_data so the accumulated time series survives across polls and restarts and
|
||||
# stations provided only by GIROIonosonde are not discarded
|
||||
ionosonde_data = dict(self._solar_conditions.ionosonde_data or {})
|
||||
updated_count = 0
|
||||
|
||||
for reading in response.json():
|
||||
station = reading.get("station", {})
|
||||
ursi = station.get("code")
|
||||
name = station.get("name")
|
||||
if not ursi or not name:
|
||||
continue
|
||||
|
||||
time_str = reading.get("time")
|
||||
if not time_str:
|
||||
continue
|
||||
try:
|
||||
ts = datetime.fromisoformat(time_str)
|
||||
if ts.tzinfo is None:
|
||||
ts = ts.replace(tzinfo=timezone.utc)
|
||||
ts_float = ts.timestamp()
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Skip readings outside our history window (some stations have months-old data)
|
||||
if ts_float < cutoff_ts:
|
||||
continue
|
||||
|
||||
fof2_val = reading.get("fof2")
|
||||
muf_val = reading.get("mufd")
|
||||
if fof2_val is None and muf_val is None:
|
||||
continue
|
||||
|
||||
# Merge this reading into the existing time series for the station.
|
||||
existing = ionosonde_data.get(ursi, {})
|
||||
fof2_dict = dict(existing.get("fof2") or {})
|
||||
muf_dict = dict(existing.get("muf") or {})
|
||||
# LUF is not available from KC2G; carry forward whatever GIRO has written.
|
||||
luf_dict = {float(t): v for t, v in (existing.get("luf") or {}).items()}
|
||||
|
||||
fof2_dict[ts_float] = fof2_val
|
||||
muf_dict[ts_float] = muf_val
|
||||
|
||||
# Trim all series to the 24-hour window.
|
||||
fof2_dict = {t: v for t, v in fof2_dict.items() if t >= cutoff_ts}
|
||||
muf_dict = {t: v for t, v in muf_dict.items() if t >= cutoff_ts}
|
||||
luf_dict = {t: v for t, v in luf_dict.items() if t >= cutoff_ts}
|
||||
|
||||
band_states = compute_band_states(fof2_dict, muf_dict, luf_dict)
|
||||
ionosonde_data[ursi] = {
|
||||
"ursi": ursi,
|
||||
"name": name,
|
||||
"fof2": fof2_dict or None,
|
||||
"muf": muf_dict or None,
|
||||
"luf": luf_dict or None,
|
||||
"band_states": band_states,
|
||||
}
|
||||
updated_count += 1
|
||||
|
||||
self.update_data({"ionosonde_data": ionosonde_data})
|
||||
self.status = "OK"
|
||||
self.last_update_time = datetime.now(pytz.UTC)
|
||||
logging.debug(f"Updated KC2G ionosonde data for {updated_count} stations.")
|
||||
|
||||
except Exception:
|
||||
self.status = "Error"
|
||||
logging.exception("Exception in KC2G ionosonde data provider")
|
||||
self._stop_event.wait(timeout=1)
|
||||
Reference in New Issue
Block a user