mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-04-30 02:35:57 +00:00
Add fetching of NOAA 3-day forecast
This commit is contained in:
2
.idea/spothole.iml
generated
2
.idea/spothole.iml
generated
@@ -4,7 +4,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.13 (spothole)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.13 virtualenv at ~/code/spothole/.venv" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -175,6 +175,10 @@ solar-condition-providers:
|
||||
class: "HamQSL"
|
||||
name: "HamQSL"
|
||||
enabled: true
|
||||
-
|
||||
class: "NOAA3dayForecast"
|
||||
name: "NOAA 3-day Forecast"
|
||||
enabled: true
|
||||
|
||||
# Port to open the local web server on
|
||||
web-server-port: 8080
|
||||
|
||||
@@ -131,6 +131,8 @@ class SolarConditions:
|
||||
hf_conditions: dict = None
|
||||
# VHF propagation conditions, keyed by condition name
|
||||
vhf_conditions: dict = None
|
||||
# NOAA Kp index 3-day forecast, keyed by ISO 8601 interval string e.g. "2026-04-02T00:00:00+00:00/2026-04-02T03:00:00+00:00"
|
||||
k_index_forecast: dict = None
|
||||
|
||||
# Derived values (populated by infer_descriptions())
|
||||
# HF radio blackout risk description, derived from x_ray
|
||||
|
||||
97
solarconditionsproviders/noaa3dayforecast.py
Normal file
97
solarconditionsproviders/noaa3dayforecast.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from solarconditionsproviders.http_solar_conditions_provider import HTTPSolarConditionsProvider
|
||||
|
||||
POLL_INTERVAL = 3600 # 1 hour
|
||||
URL = "https://services.swpc.noaa.gov/text/3-day-forecast.txt"
|
||||
|
||||
|
||||
class NOAA3dayForecast(HTTPSolarConditionsProvider):
|
||||
"""Solar conditions provider using the NOAA 3-day forecast text file. Parses the NOAA forecast and populates
|
||||
corresponding fields in the solar conditions object.."""
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(provider_config, URL, POLL_INTERVAL)
|
||||
|
||||
def _http_response_to_solar_conditions(self, http_response):
|
||||
if http_response.status_code != 200:
|
||||
logging.warning("NOAA K-index forecast API returned HTTP " + str(http_response.status_code))
|
||||
return None
|
||||
|
||||
lines = http_response.text.splitlines()
|
||||
|
||||
# Find the "NOAA Kp index breakdown" section header
|
||||
start_idx = None
|
||||
for i, line in enumerate(lines):
|
||||
if "NOAA Kp index breakdown" in line:
|
||||
start_idx = i
|
||||
break
|
||||
|
||||
if start_idx is None:
|
||||
logging.warning("NOAA K-index forecast: could not find 'NOAA Kp index breakdown' section")
|
||||
return None
|
||||
|
||||
# Extract the year from the header line, e.g. "NOAA Kp index breakdown Apr 2-Apr 4, 2026"
|
||||
header_line = lines[start_idx]
|
||||
year_match = re.search(r'\b(\d{4})\b', header_line)
|
||||
if not year_match:
|
||||
logging.warning("NOAA K-index forecast: could not extract year from: " + header_line)
|
||||
return None
|
||||
year = int(year_match.group(1))
|
||||
|
||||
# Parse the column date headers on the next line, e.g. " Apr 02 Apr 03 Apr 04"
|
||||
if start_idx + 1 >= len(lines):
|
||||
logging.warning("NOAA K-index forecast: missing date header line")
|
||||
return None
|
||||
|
||||
date_header_line = lines[start_idx + 2]
|
||||
date_matches = re.findall(r'([A-Za-z]{3})\s+(\d{2})', date_header_line)
|
||||
if not date_matches:
|
||||
logging.warning("NOAA K-index forecast: could not parse date headers from: " + date_header_line)
|
||||
return None
|
||||
|
||||
column_dates = []
|
||||
for month_str, day_str in date_matches:
|
||||
try:
|
||||
column_dates.append(datetime.strptime(f"{day_str} {month_str} {year}", "%d %b %Y").date())
|
||||
except ValueError:
|
||||
logging.warning(f"NOAA K-index forecast: could not parse date: {month_str} {day_str} {year}")
|
||||
return None
|
||||
|
||||
# Parse each data row, e.g. "00-03UT 2.00 3.00 2.00"
|
||||
k_index_forecast = {}
|
||||
for line in lines[start_idx + 3:]:
|
||||
time_match = re.match(r'^(\d{2})-(\d{2})UT\s+(.*)', line.strip())
|
||||
if not time_match:
|
||||
if k_index_forecast:
|
||||
break
|
||||
continue
|
||||
|
||||
start_hour = int(time_match.group(1))
|
||||
raw_values = time_match.group(3).split()
|
||||
|
||||
for i, val in enumerate(raw_values):
|
||||
if i >= len(column_dates):
|
||||
break
|
||||
# Discard bracketed values
|
||||
if val.startswith('(') and val.endswith(')'):
|
||||
continue
|
||||
try:
|
||||
kp = float(val)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
date = column_dates[i]
|
||||
start_dt = datetime(date.year, date.month, date.day, start_hour, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
# Key the data dict by start time
|
||||
key = start_dt.timestamp()
|
||||
k_index_forecast[key] = kp
|
||||
|
||||
if not k_index_forecast:
|
||||
logging.warning("NOAA K-index forecast: no data rows parsed")
|
||||
return None
|
||||
|
||||
return {"k_index_forecast": k_index_forecast}
|
||||
@@ -67,7 +67,7 @@
|
||||
<p>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.</p>
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775226906"></script>
|
||||
<script src="/js/common.js?v=1775232636"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-about").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -69,8 +69,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775226906"></script>
|
||||
<script src="/js/add-spot.js?v=1775226906"></script>
|
||||
<script src="/js/common.js?v=1775232636"></script>
|
||||
<script src="/js/add-spot.js?v=1775232636"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -56,8 +56,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775226906"></script>
|
||||
<script src="/js/alerts.js?v=1775226906"></script>
|
||||
<script src="/js/common.js?v=1775232636"></script>
|
||||
<script src="/js/alerts.js?v=1775232636"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -62,9 +62,9 @@
|
||||
<script>
|
||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||
</script>
|
||||
<script src="/js/common.js?v=1775226906"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1775226906"></script>
|
||||
<script src="/js/bands.js?v=1775226906"></script>
|
||||
<script src="/js/common.js?v=1775232636"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1775232636"></script>
|
||||
<script src="/js/bands.js?v=1775232636"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -46,10 +46,10 @@
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tinycolor2@1.6.0/cjs/tinycolor.min.js"></script>
|
||||
|
||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1775226906"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1775226906"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1775226906"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1775226906"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1775232636"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1775232636"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1775232636"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1775232636"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -189,8 +189,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775226906"></script>
|
||||
<script src="/js/conditions.js?v=1775226906"></script>
|
||||
<script src="/js/common.js?v=1775232636"></script>
|
||||
<script src="/js/conditions.js?v=1775232636"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-conditions").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -70,9 +70,9 @@
|
||||
<script>
|
||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||
</script>
|
||||
<script src="/js/common.js?v=1775226906"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1775226906"></script>
|
||||
<script src="/js/map.js?v=1775226906"></script>
|
||||
<script src="/js/common.js?v=1775232636"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1775232636"></script>
|
||||
<script src="/js/map.js?v=1775232636"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -87,9 +87,9 @@
|
||||
<script>
|
||||
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
|
||||
</script>
|
||||
<script src="/js/common.js?v=1775226906"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1775226906"></script>
|
||||
<script src="/js/spots.js?v=1775226906"></script>
|
||||
<script src="/js/common.js?v=1775232636"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1775232636"></script>
|
||||
<script src="/js/spots.js?v=1775232636"></script>
|
||||
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
|
||||
|
||||
{% end %}
|
||||
@@ -59,8 +59,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1775226906"></script>
|
||||
<script src="/js/status.js?v=1775226906"></script>
|
||||
<script src="/js/common.js?v=1775232636"></script>
|
||||
<script src="/js/status.js?v=1775232636"></script>
|
||||
<script>
|
||||
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
|
||||
</script>
|
||||
|
||||
@@ -1507,6 +1507,17 @@ components:
|
||||
type: string
|
||||
description: Sporadic-E propagation condition on 2m for North America
|
||||
example: "Band Closed"
|
||||
k_index_forecast:
|
||||
type: object
|
||||
description: NOAA Kp index 3-day forecast. Keys UNIX time in seconds representing the start of the relevant 3-hour UTC period. Values are the forecast Kp index (0–9) for that period.
|
||||
additionalProperties:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 9
|
||||
example:
|
||||
"2026-04-02T00:00:00+00:00": 2
|
||||
"2026-04-02T03:00:00+00:00": 3
|
||||
"2026-04-02T06:00:00+00:00": 2
|
||||
blackout_desc:
|
||||
type: string
|
||||
description: HF radio blackout risk description, derived from the X-ray flux class.
|
||||
|
||||
Reference in New Issue
Block a user