mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-04-29 18:25:58 +00:00
Compare commits
7 Commits
ee47d736eb
...
7de3cdc49c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7de3cdc49c | ||
|
|
6f0101a861 | ||
|
|
4fe8dfc36a | ||
|
|
44f38b8114 | ||
|
|
5de5a7ffdf | ||
|
|
ed1f9e5b06 | ||
|
|
11d71629ce |
@@ -6,9 +6,9 @@ from dataclasses import dataclass
|
||||
# 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",
|
||||
"X": "Wide area HF radio blackout across sunlit side",
|
||||
"M": "Occasional loss of HF communications on sunlit side",
|
||||
"C": "Low absorption of HF signals on sunlit side",
|
||||
"B": "No significant radio blackout",
|
||||
"A": "No impact",
|
||||
}
|
||||
@@ -33,16 +33,16 @@ SOLAR_STORM_SCALES = [
|
||||
]
|
||||
|
||||
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"),
|
||||
(9, "Complete HF blackout"),
|
||||
(8, "HF sporadic only"),
|
||||
(7, "HF intermittent"),
|
||||
(6, "HF fading at higher latitudes"),
|
||||
(5, "HF fading at higher latitudes"),
|
||||
(4, "Minor HF fading at higher latitudes"),
|
||||
(3, "Minor HF fading at higher latitudes"),
|
||||
(2, "No impact"),
|
||||
(1, "No impact"),
|
||||
(0, "No impact"),
|
||||
]
|
||||
|
||||
GEOMAG_STORM_SCALES = [
|
||||
@@ -95,18 +95,6 @@ class HFBandCondition:
|
||||
condition: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class VHFCondition:
|
||||
"""Data class representing a VHF propagation condition."""
|
||||
|
||||
# Phenomenon name, e.g. "E-Skip", "Sporadic E"
|
||||
phenomenon: str = None
|
||||
# Geographic location this applies to, e.g. "Europe", "N America"
|
||||
location: str = None
|
||||
# Condition description, e.g. "Band Closed", "Enhanced", "Good"
|
||||
condition: str = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SolarConditions:
|
||||
"""Data class representing current solar and propagation conditions."""
|
||||
@@ -139,13 +127,13 @@ class SolarConditions:
|
||||
geomag_field: str = None
|
||||
# Geomagnetic background noise level, e.g. "S0", "S1", "S2"
|
||||
geomag_noise: str = None
|
||||
# HF band propagation conditions
|
||||
hf_conditions: list = None # list[HFBandCondition]
|
||||
# VHF propagation phenomena
|
||||
vhf_conditions: list = None # list[VHFCondition]
|
||||
# HF band propagation conditions, keyed by "{band}-{time}" e.g. "80m-40m-day"
|
||||
hf_conditions: dict = None
|
||||
# VHF propagation conditions, keyed by condition name
|
||||
vhf_conditions: dict = None
|
||||
|
||||
# Derived values (populated by infer_descriptions())
|
||||
# HF radio blackout risk, derived from x_ray
|
||||
# HF radio blackout risk description, derived from x_ray
|
||||
blackout_desc: str = None
|
||||
# Solar radiation storm level description, derived from proton_flux
|
||||
proton_flux_desc: str = None
|
||||
@@ -157,7 +145,7 @@ class SolarConditions:
|
||||
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 description, derived from electron_flux
|
||||
electron_flux_desc: str = None
|
||||
|
||||
def infer_descriptions(self):
|
||||
|
||||
49
server/handlers/api/dxstats.py
Normal file
49
server/handlers/api/dxstats.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
from collections import Counter
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
import tornado
|
||||
|
||||
from core.prometheus_metrics_handler import api_requests_counter
|
||||
|
||||
CONTINENTS = ["EU", "NA", "SA", "AS", "AF", "OC", "AN"]
|
||||
BANDS = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"]
|
||||
CONTINENTS_SET = frozenset(CONTINENTS)
|
||||
BANDS_SET = frozenset(BANDS)
|
||||
|
||||
|
||||
class APIDxStatsHandler(tornado.web.RequestHandler):
|
||||
"""API request handler for /api/v1/dxstats"""
|
||||
|
||||
def initialize(self, spots, web_server_metrics):
|
||||
self._spots = spots
|
||||
self._web_server_metrics = web_server_metrics
|
||||
|
||||
def get(self):
|
||||
self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC)
|
||||
self._web_server_metrics["api_access_counter"] += 1
|
||||
self._web_server_metrics["status"] = "OK"
|
||||
api_requests_counter.inc()
|
||||
|
||||
one_hour_ago = (datetime.now(pytz.UTC) - timedelta(hours=1)).timestamp()
|
||||
counts = Counter()
|
||||
|
||||
for key in self._spots.iterkeys():
|
||||
spot = self._spots.get(key)
|
||||
if spot is None:
|
||||
continue
|
||||
if not spot.time or spot.time < one_hour_ago:
|
||||
continue
|
||||
if spot.de_continent in CONTINENTS_SET and spot.dx_continent in CONTINENTS_SET and spot.band in BANDS_SET:
|
||||
counts[spot.de_continent, spot.dx_continent, spot.band] += 1
|
||||
|
||||
result = {
|
||||
de: {dx: {band: counts[de, dx, band] for band in BANDS} for dx in CONTINENTS}
|
||||
for de in CONTINENTS
|
||||
}
|
||||
|
||||
self.write(json.dumps(result))
|
||||
self.set_status(200)
|
||||
self.set_header("Cache-Control", "no-store")
|
||||
self.set_header("Content-Type", "application/json")
|
||||
@@ -7,6 +7,7 @@ from tornado.web import StaticFileHandler
|
||||
|
||||
from core.utils import empty_queue
|
||||
from server.handlers.api.addspot import APISpotHandler
|
||||
from server.handlers.api.dxstats import APIDxStatsHandler
|
||||
from server.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
||||
from server.handlers.api.lookups import APILookupCallHandler, APILookupSIGRefHandler, APILookupGridHandler
|
||||
from server.handlers.api.options import APIOptionsHandler
|
||||
@@ -63,6 +64,7 @@ class WebServer:
|
||||
{"sse_alert_queues": self._sse_alert_queues, "web_server_metrics": self.web_server_metrics}),
|
||||
(r"/api/v1/solar", APISolarConditionsHandler,
|
||||
{"solar_conditions": self._solar_conditions, "web_server_metrics": self.web_server_metrics}),
|
||||
(r"/api/v1/dxstats", APIDxStatsHandler, {"spots": self._spots, "web_server_metrics": self.web_server_metrics}),
|
||||
(r"/api/v1/options", APIOptionsHandler,
|
||||
{"status_data": self._status_data, "web_server_metrics": self.web_server_metrics}),
|
||||
(r"/api/v1/status", APIStatusHandler,
|
||||
|
||||
@@ -4,7 +4,7 @@ from xml.etree import ElementTree
|
||||
import pytz
|
||||
from dateutil import parser as dateutil_parser, tz as dateutil_tz
|
||||
|
||||
from data.solar_conditions import HFBandCondition, VHFCondition
|
||||
|
||||
from solarconditionsproviders.http_solar_conditions_provider import HTTPSolarConditionsProvider
|
||||
|
||||
POLL_INTERVAL = 3600 # 1 hour
|
||||
@@ -48,7 +48,7 @@ class HamQSL(HTTPSolarConditionsProvider):
|
||||
return default
|
||||
|
||||
# Process HF band conditions
|
||||
hf_conditions = []
|
||||
hf_conditions = {}
|
||||
calc = sd.find("calculatedconditions")
|
||||
if calc is not None:
|
||||
for band_el in calc.findall("band"):
|
||||
@@ -56,18 +56,15 @@ class HamQSL(HTTPSolarConditionsProvider):
|
||||
time = band_el.get("time")
|
||||
condition = band_el.text.strip() if band_el.text else None
|
||||
if name and time and condition:
|
||||
hf_conditions.append(HFBandCondition(band=name, time=time, condition=condition))
|
||||
hf_conditions[f"{name}-{time}"] = condition
|
||||
|
||||
# Process VHF propagation conditions
|
||||
vhf_conditions = []
|
||||
vhf_map = {}
|
||||
vhf = sd.find("calculatedvhfconditions")
|
||||
if vhf is not None:
|
||||
for ph_el in vhf.findall("phenomenon"):
|
||||
vhf_conditions.append(VHFCondition(
|
||||
phenomenon=ph_el.get("name"),
|
||||
location=ph_el.get("location"),
|
||||
condition=ph_el.text.strip() if ph_el.text else None
|
||||
))
|
||||
key = (ph_el.get("name"), ph_el.get("location"))
|
||||
vhf_map[key] = ph_el.text.strip() if ph_el.text else None
|
||||
|
||||
# Parse the "updated" timestamp string (format: "28 Mar 2026 0949 GMT") to UTC epoch seconds.
|
||||
updated = None
|
||||
@@ -97,8 +94,14 @@ class HamQSL(HTTPSolarConditionsProvider):
|
||||
"aurora_latitude": float_val("latdegree"),
|
||||
"solar_wind": float_val("solarwind"),
|
||||
"magnetic_field": float_val("magneticfield"),
|
||||
"geomag_field": text("geomagfield"),
|
||||
"geomag_field": (lambda v: "Unsettled" if v == "Unsettld" else v)(text("geomagfield").title()),
|
||||
"geomag_noise": text("signalnoise"),
|
||||
"hf_conditions": hf_conditions,
|
||||
"vhf_conditions": vhf_conditions
|
||||
"vhf_conditions": {
|
||||
"vhf_aurora_northern_hemi": vhf_map.get(("vhf-aurora", "northern_hemi")).title().replace("Lat Aur", "Latitude"),
|
||||
"es_2m_europe": vhf_map.get(("E-Skip", "europe")),
|
||||
"es_4m_europe": vhf_map.get(("E-Skip", "europe_4m")),
|
||||
"es_6m_europe": vhf_map.get(("E-Skip", "europe_6m")),
|
||||
"es_2m_na": vhf_map.get(("E-Skip", "north_america")),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class ParksNPeaks(HTTPSpotProvider):
|
||||
def _http_response_to_spots(self, http_response):
|
||||
new_spots = []
|
||||
# Iterate through source data
|
||||
if http_response and http_response != "":
|
||||
for source_spot in http_response.json():
|
||||
# Convert to our spot format
|
||||
spot = Spot(source=self.name,
|
||||
|
||||
@@ -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=1774699418"></script>
|
||||
<script src="/js/common.js?v=1774778476"></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=1774699418"></script>
|
||||
<script src="/js/add-spot.js?v=1774699418"></script>
|
||||
<script src="/js/common.js?v=1774778476"></script>
|
||||
<script src="/js/add-spot.js?v=1774778476"></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=1774699419"></script>
|
||||
<script src="/js/alerts.js?v=1774699419"></script>
|
||||
<script src="/js/common.js?v=1774778476"></script>
|
||||
<script src="/js/alerts.js?v=1774778476"></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=1774699418"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1774699418"></script>
|
||||
<script src="/js/bands.js?v=1774699418"></script>
|
||||
<script src="/js/common.js?v=1774778476"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1774778476"></script>
|
||||
<script src="/js/bands.js?v=1774778476"></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=1774699418"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1774699418"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1774699418"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1774699418"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/utils.js?v=1774778476"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/storage.js?v=1774778476"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/ui-ham.js?v=1774778476"></script>
|
||||
<script src="https://misc.ianrenton.com/jsutils/geo.js?v=1774778476"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3,25 +3,91 @@
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">
|
||||
Propagation
|
||||
Propagation Conditions
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 row-cols-md-2 g-3">
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">HF Conditions</h5>
|
||||
<h5 class="card-title">HF</h5>
|
||||
<table class="table table-sm mt-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Band</th>
|
||||
<th>Day</th>
|
||||
<th>Night</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>80-40m</td>
|
||||
<td id="hf-conditions-80m-40m-day"></td>
|
||||
<td id="hf-conditions-80m-40m-night"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>30-20m</td>
|
||||
<td id="hf-conditions-30m-20m-day"></td>
|
||||
<td id="hf-conditions-30m-20m-night"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>17-15m</td>
|
||||
<td id="hf-conditions-17m-15m-day"></td>
|
||||
<td id="hf-conditions-17m-15m-night"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>12-10m</td>
|
||||
<td id="hf-conditions-12m-10m-day"></td>
|
||||
<td id="hf-conditions-12m-10m-night"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">VHF Conditions</h5>
|
||||
<h5 class="card-title">VHF</h5>
|
||||
<table class="table table-sm mt-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Propagation Mode</th>
|
||||
<th>Condition</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Sporadic-E 6m (Europe)</td>
|
||||
<td id="vhf-conditions-es_6m_europe"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sporadic-E 4m (Europe)</td>
|
||||
<td id="vhf-conditions-es_4m_europe"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sporadic-E 2m (Europe)</td>
|
||||
<td id="vhf-conditions-es_2m_europe"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Sporadic-E 2m (North America)</td>
|
||||
<td id="vhf-conditions-es_2m_na"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aurora (Northern Hemisphere)</td>
|
||||
<td id="vhf-conditions-vhf_aurora_northern_hemi"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aurora Minimum Latitude</td>
|
||||
<td id="vhf-conditions-aurora-lat"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text mt-3">Data from <a href="https://hamqsl.com">HamQSL.com</a>.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,12 +96,101 @@
|
||||
Solar Weather
|
||||
</div>
|
||||
<div class="card-body">
|
||||
Coming soon!
|
||||
<div class="row border-bottom align-items-start me-0">
|
||||
<div class="col-12 col-md-2 py-2 fw-bold">Solar Flux</div>
|
||||
<div id="sw-solar-flux-vals" class="col-12 col-md-3 py-2">
|
||||
<span class="me-3">SFI: <strong id="sw-sfi"></strong></span>
|
||||
<span>Sunspots: <strong id="sw-sunspots"></strong></span>
|
||||
</div>
|
||||
<div id="sw-solar-flux-desc" class="col-12 col-md-7 py-2"></div>
|
||||
</div>
|
||||
<div class="row border-bottom align-items-start me-0">
|
||||
<div class="col-12 col-md-2 py-2 fw-bold">Geomagnetic</div>
|
||||
<div id="sw-geomag-vals" class="col-12 col-md-3 py-2">
|
||||
<span class="me-3">K: <strong id="sw-k-index"></strong></span>
|
||||
<span class="me-3">A: <strong id="sw-a-index"></strong></span>
|
||||
<span class="me-3"><strong>G</strong><strong id="sw-geomag-storm-scale"></strong></span>
|
||||
<span>Noise: <strong id="sw-geomag-noise"></strong></span>
|
||||
</div>
|
||||
<div id="sw-geomag-desc" class="col-12 col-md-7 py-2">
|
||||
<span id="sw-geomag-field"></span>. <span id="sw-geomag-storm-desc"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row border-bottom align-items-start me-0">
|
||||
<div class="col-12 col-md-2 py-2 fw-bold">X-ray Flux</div>
|
||||
<div id="sw-xray-vals" class="col-12 col-md-3 py-2"><strong id="sw-x-ray"></strong></div>
|
||||
<div id="sw-xray-desc" class="col-12 col-md-7 py-2"></div>
|
||||
</div>
|
||||
<div class="row border-bottom align-items-start me-0">
|
||||
<div class="col-12 col-md-2 py-2 fw-bold">Proton Flux</div>
|
||||
<div id="sw-proton-vals" class="col-12 col-md-3 py-2">
|
||||
<span class="me-3"><strong id="sw-proton-flux"></strong> pfu</span>
|
||||
<span class="me-3"><strong>S</strong><strong id="sw-solar-storm-scale"></strong></span>
|
||||
</div>
|
||||
<div id="sw-proton-desc" class="col-12 col-md-7 py-2"></div>
|
||||
</div>
|
||||
<div class="row border-bottom align-items-start me-0">
|
||||
<div class="col-12 col-md-2 fw-bold py-2">Electron Flux</div>
|
||||
<div id="sw-electron-vals" class="col-12 col-md-3 py-2"><strong id="sw-electron-flux"></strong> efu</div>
|
||||
<div id="sw-electron-desc" class="col-12 col-md-7 py-2"></div>
|
||||
</div>
|
||||
<div class="form-text mt-3">Data from <a href="https://hamqsl.com">HamQSL.com</a>.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1774699418"></script>
|
||||
<script src="/js/conditions.js?v=1774699418"></script>
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">
|
||||
DX Opportunities
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="dxstats-de-continent" class="form-label">Your continent:</label>
|
||||
<select id="dxstats-de-continent" class="form-select storeable-select d-inline-block ms-2" style="width: auto;" oninput="dxStatsContientChanged();">
|
||||
<option value="EU">Europe</option>
|
||||
<option value="NA">North America</option>
|
||||
<option value="SA">South America</option>
|
||||
<option value="AS">Asia</option>
|
||||
<option value="AF">Africa</option>
|
||||
<option value="OC">Oceania</option>
|
||||
<option value="AN">Antarctica</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>160m</th>
|
||||
<th>80m</th>
|
||||
<th>60m</th>
|
||||
<th>40m</th>
|
||||
<th>30m</th>
|
||||
<th>20m</th>
|
||||
<th>17m</th>
|
||||
<th>15m</th>
|
||||
<th>12m</th>
|
||||
<th>10m</th>
|
||||
<th>6m</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for continent in ["EU", "NA", "SA", "AS", "AF", "OC", "AN"] %}
|
||||
<tr>
|
||||
<td class="fw-bold">{{ continent }}</td>
|
||||
{% for band in ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"] %}
|
||||
<td id="dxstats-{{ continent }}-{{ band }}"></td>
|
||||
{% end %}
|
||||
</tr>
|
||||
{% end %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form-text mt-2">This table shows the number of spots in the past hour received in your continent, where the DX continent and band are as shown in the table. Bands with high numbers of spots are likely to be the best ones for making contact with the continent you want right now. Bear in mind that some bands and some continents are inherently much rarer than others.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/common.js?v=1774778476"></script>
|
||||
<script src="/js/conditions.js?v=1774778476"></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=1774699419"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1774699419"></script>
|
||||
<script src="/js/map.js?v=1774699419"></script>
|
||||
<script src="/js/common.js?v=1774778476"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1774778476"></script>
|
||||
<script src="/js/map.js?v=1774778476"></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=1774699418"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1774699418"></script>
|
||||
<script src="/js/spots.js?v=1774699418"></script>
|
||||
<script src="/js/common.js?v=1774778476"></script>
|
||||
<script src="/js/spotsbandsandmap.js?v=1774778476"></script>
|
||||
<script src="/js/spots.js?v=1774778476"></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=1774699418"></script>
|
||||
<script src="/js/status.js?v=1774699418"></script>
|
||||
<script src="/js/common.js?v=1774778476"></script>
|
||||
<script src="/js/status.js?v=1774778476"></script>
|
||||
<script>
|
||||
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
|
||||
</script>
|
||||
|
||||
@@ -15,6 +15,7 @@ info:
|
||||
|
||||
### 1.2
|
||||
|
||||
* Added `/dxstats` endpoint for inter-continent DX spot statistics.
|
||||
* Added `/solar` endpoint for solar and propagation conditions.
|
||||
* Added `solar_condition_providers` array to the `/status` response.
|
||||
|
||||
@@ -406,6 +407,61 @@ paths:
|
||||
$ref: '#/components/schemas/AlertStream'
|
||||
|
||||
|
||||
/solar:
|
||||
get:
|
||||
tags:
|
||||
- Propagation & DX
|
||||
summary: Get solar and band conditions
|
||||
description: Returns the current solar conditions and HF/VHF propagation condition summaries. This data is sourced from external providers (e.g. HamQSL) and updated periodically. All fields may be null if no provider has successfully fetched data yet.
|
||||
operationId: solar
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SolarConditions'
|
||||
|
||||
|
||||
/dxstats:
|
||||
get:
|
||||
tags:
|
||||
- Propagation & DX
|
||||
summary: Get spot counts by continent and band
|
||||
description: Returns a three-level nested object of spot counts from the current spot database, grouped by DE continent, then DX continent, then band. Only spots in the last hour are counted, regardless of what the server owner has set the spot expiry time to.
|
||||
operationId: dxstats
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
description: Spot counts keyed by DE continent
|
||||
additionalProperties:
|
||||
type: object
|
||||
description: Spot counts keyed by DX continent
|
||||
additionalProperties:
|
||||
type: object
|
||||
description: Spot counts keyed by band
|
||||
properties:
|
||||
160m: { type: integer }
|
||||
80m: { type: integer }
|
||||
60m: { type: integer }
|
||||
40m: { type: integer }
|
||||
30m: { type: integer }
|
||||
20m: { type: integer }
|
||||
17m: { type: integer }
|
||||
15m: { type: integer }
|
||||
12m: { type: integer }
|
||||
10m: { type: integer }
|
||||
6m: { type: integer }
|
||||
example:
|
||||
EU:
|
||||
NA: { 20m: 42, 17m: 7, 15m: 3, 10m: 0, 6m: 0, 160m: 0, 80m: 1, 60m: 0, 40m: 5, 30m: 2, 12m: 0 }
|
||||
EU: { 20m: 18, 17m: 2, 15m: 0, 10m: 0, 6m: 1, 160m: 0, 80m: 4, 60m: 0, 40m: 9, 30m: 1, 12m: 0 }
|
||||
|
||||
|
||||
/status:
|
||||
get:
|
||||
tags:
|
||||
@@ -781,23 +837,6 @@ paths:
|
||||
type: string
|
||||
example: "Failed"
|
||||
|
||||
|
||||
/solar:
|
||||
get:
|
||||
tags:
|
||||
- General
|
||||
summary: Get solar and propagation conditions
|
||||
description: Returns the current solar conditions and HF/VHF propagation condition summaries. This data is sourced from external providers (e.g. HamQSL) and updated periodically. All fields may be null if no provider has successfully fetched data yet.
|
||||
operationId: solar
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SolarConditions'
|
||||
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Source:
|
||||
@@ -1177,7 +1216,7 @@ components:
|
||||
SpotStream:
|
||||
type: object
|
||||
description: A server-sent event containing a spot
|
||||
required: [data]
|
||||
required: [ data ]
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/Spot"
|
||||
@@ -1278,7 +1317,7 @@ components:
|
||||
AlertStream:
|
||||
type: object
|
||||
description: A server-sent event containing an alert
|
||||
required: [data]
|
||||
required: [ data ]
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/components/schemas/Alert"
|
||||
@@ -1356,47 +1395,6 @@ components:
|
||||
description: Regex that matches this SIG's reference IDs. Generally for Spothole's own internal use, clients probably won't need this.
|
||||
example: "[A-Z]{2}\\-\\d+"
|
||||
|
||||
HFBandCondition:
|
||||
type: object
|
||||
description: HF propagation conditions for a group of bands at a particular time of day.
|
||||
properties:
|
||||
band:
|
||||
type: string
|
||||
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. As provided by HamQSL.
|
||||
enum:
|
||||
- day
|
||||
- night
|
||||
example: day
|
||||
condition:
|
||||
type: string
|
||||
description: Propagation condition assessment. As provided by HamQSL.
|
||||
enum:
|
||||
- Good
|
||||
- Fair
|
||||
- Poor
|
||||
example: Good
|
||||
|
||||
VHFCondition:
|
||||
type: object
|
||||
description: A VHF propagation phenomenon and its current condition.
|
||||
properties:
|
||||
phenomenon:
|
||||
type: string
|
||||
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", "north_america", "northern_hemi". As provided by HamQSL.
|
||||
example: "europe"
|
||||
condition:
|
||||
type: string
|
||||
description: The current condition for this phenomenon and location.
|
||||
example: "Band Closed"
|
||||
|
||||
SolarConditions:
|
||||
type: object
|
||||
description: Current solar and propagation conditions. All fields may be null if no provider has successfully fetched data yet.
|
||||
@@ -1458,15 +1456,57 @@ components:
|
||||
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
|
||||
items:
|
||||
$ref: '#/components/schemas/HFBandCondition'
|
||||
type: object
|
||||
description: HF propagation condition assessments, keyed by "{band}-{time}" e.g. "80m-40m-day"
|
||||
properties:
|
||||
80m-40m-day:
|
||||
type: string
|
||||
enum: [ Good, Fair, Poor ]
|
||||
80m-40m-night:
|
||||
type: string
|
||||
enum: [ Good, Fair, Poor ]
|
||||
30m-20m-day:
|
||||
type: string
|
||||
enum: [ Good, Fair, Poor ]
|
||||
30m-20m-night:
|
||||
type: string
|
||||
enum: [ Good, Fair, Poor ]
|
||||
17m-15m-day:
|
||||
type: string
|
||||
enum: [ Good, Fair, Poor ]
|
||||
17m-15m-night:
|
||||
type: string
|
||||
enum: [ Good, Fair, Poor ]
|
||||
12m-10m-day:
|
||||
type: string
|
||||
enum: [ Good, Fair, Poor ]
|
||||
12m-10m-night:
|
||||
type: string
|
||||
enum: [ Good, Fair, Poor ]
|
||||
vhf_conditions:
|
||||
type: array
|
||||
description: VHF propagation condition assessments by phenomenon and location
|
||||
items:
|
||||
$ref: '#/components/schemas/VHFCondition'
|
||||
type: object
|
||||
description: VHF propagation condition assessments, keyed by condition name
|
||||
properties:
|
||||
vhf_aurora_northern_hemi:
|
||||
type: string
|
||||
description: VHF aurora propagation condition for the northern hemisphere
|
||||
example: "Band Closed"
|
||||
es_2m_europe:
|
||||
type: string
|
||||
description: Sporadic-E propagation condition on 2m for Europe
|
||||
example: "Band Closed"
|
||||
es_4m_europe:
|
||||
type: string
|
||||
description: Sporadic-E propagation condition on 4m for Europe
|
||||
example: "Band Closed"
|
||||
es_6m_europe:
|
||||
type: string
|
||||
description: Sporadic-E propagation condition on 6m for Europe
|
||||
example: "Band Closed"
|
||||
es_2m_na:
|
||||
type: string
|
||||
description: Sporadic-E propagation condition on 2m for North America
|
||||
example: "Band Closed"
|
||||
blackout_desc:
|
||||
type: string
|
||||
description: HF radio blackout risk description, derived from the X-ray flux class.
|
||||
|
||||
@@ -1,11 +1,157 @@
|
||||
// Cache for the full dxstats API response, so we can reload on the fly if the user changes the value of their continent
|
||||
// in the select box
|
||||
let dxStatsData = null;
|
||||
|
||||
// Load solar conditions
|
||||
function loadSolarConditions() {
|
||||
$.getJSON('/api/v1/solar', function(jsonData) {
|
||||
|
||||
// HF
|
||||
|
||||
const hfConditionClass = { 'Good': 'table-success', 'Fair': 'table-warning', 'Poor': 'table-danger' };
|
||||
|
||||
if (jsonData.hf_conditions) {
|
||||
Object.entries(jsonData.hf_conditions).forEach(function([key, condition]) {
|
||||
const cell = $('#hf-conditions-' + key);
|
||||
cell.text(condition);
|
||||
cell.addClass(hfConditionClass[condition]);
|
||||
});
|
||||
}
|
||||
|
||||
// VHF
|
||||
|
||||
if (jsonData.vhf_conditions) {
|
||||
Object.entries(jsonData.vhf_conditions).forEach(function([key, condition]) {
|
||||
const cell = $('#vhf-conditions-' + key);
|
||||
cell.text(condition);
|
||||
let vhfClass;
|
||||
if (condition === 'Band Closed') {
|
||||
vhfClass = 'table-danger';
|
||||
} else if (condition.includes('High')) {
|
||||
vhfClass = 'table-warning';
|
||||
} else {
|
||||
vhfClass = 'table-success';
|
||||
}
|
||||
cell.addClass(vhfClass);
|
||||
});
|
||||
}
|
||||
if (jsonData.aurora_latitude !== null && jsonData.aurora_latitude !== undefined) {
|
||||
$('#vhf-conditions-aurora-lat').text(jsonData.aurora_latitude + '°');
|
||||
}
|
||||
|
||||
// Solar Weather
|
||||
|
||||
const swFields = {
|
||||
'sfi': 'sw-sfi',
|
||||
'sunspots': 'sw-sunspots',
|
||||
'band_conditions_desc': 'sw-solar-flux-desc',
|
||||
'k_index': 'sw-k-index',
|
||||
'a_index': 'sw-a-index',
|
||||
'geomag_field': 'sw-geomag-field',
|
||||
'geomag_storm_scale': 'sw-geomag-storm-scale',
|
||||
'geomag_storm_desc': 'sw-geomag-storm-desc',
|
||||
'geomag_noise': 'sw-geomag-noise',
|
||||
'x_ray': 'sw-x-ray',
|
||||
'blackout_desc': 'sw-xray-desc',
|
||||
'proton_flux': 'sw-proton-flux',
|
||||
'solar_storm_scale': 'sw-solar-storm-scale',
|
||||
'proton_flux_desc': 'sw-proton-desc',
|
||||
'electron_flux': 'sw-electron-flux',
|
||||
'electron_flux_desc': 'sw-electron-desc',
|
||||
};
|
||||
Object.entries(swFields).forEach(function([field, id]) {
|
||||
const val = jsonData[field];
|
||||
if (val !== null && val !== undefined) {
|
||||
$('#' + id).text(val);
|
||||
}
|
||||
});
|
||||
|
||||
// Solar Weather - colouring
|
||||
|
||||
function applySwClass(valsId, descId, cls) {
|
||||
$('#' + valsId).addClass(cls);
|
||||
$('#' + descId).addClass(cls);
|
||||
}
|
||||
|
||||
const sfi = jsonData.sfi;
|
||||
if (sfi !== null && sfi !== undefined) {
|
||||
applySwClass('sw-solar-flux-vals', 'sw-solar-flux-desc',
|
||||
sfi > 150 ? 'bg-success-subtle' : sfi > 90 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
||||
}
|
||||
|
||||
const kIndex = jsonData.k_index;
|
||||
if (kIndex !== null && kIndex !== undefined) {
|
||||
applySwClass('sw-geomag-vals', 'sw-geomag-desc',
|
||||
kIndex < 5 ? 'bg-success-subtle' : kIndex < 7 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
||||
}
|
||||
|
||||
const xRay = jsonData.x_ray;
|
||||
if (xRay) {
|
||||
const letter = xRay[0].toUpperCase();
|
||||
const xRayClass = (letter === 'X') ? 'bg-danger-subtle'
|
||||
: (letter === 'M') ? 'bg-warning-subtle'
|
||||
: 'bg-success-subtle';
|
||||
applySwClass('sw-xray-vals', 'sw-xray-desc', xRayClass);
|
||||
}
|
||||
|
||||
const protonFlux = jsonData.proton_flux;
|
||||
if (protonFlux !== null && protonFlux !== undefined) {
|
||||
applySwClass('sw-proton-vals', 'sw-proton-desc',
|
||||
protonFlux <= 100 ? 'bg-success-subtle' : protonFlux <= 10000 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
||||
}
|
||||
|
||||
const electronFlux = jsonData.electron_flux;
|
||||
if (electronFlux !== null && electronFlux !== undefined) {
|
||||
applySwClass('sw-electron-vals', 'sw-electron-desc',
|
||||
electronFlux <= 100 ? 'bg-success-subtle' : electronFlux <= 1000 ? 'bg-warning-subtle' : 'bg-danger-subtle');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render the DX stats table for the currently selected DE continent
|
||||
function renderDxStats() {
|
||||
if (!dxStatsData) { return; }
|
||||
const deContinent = $('#dxstats-de-continent').val();
|
||||
const deData = dxStatsData[deContinent];
|
||||
if (!deData) { return; }
|
||||
|
||||
const cells = [];
|
||||
Object.entries(deData).forEach(function([dxContinent, bands]) {
|
||||
Object.entries(bands).forEach(function([band, count]) {
|
||||
const cell = $('#dxstats-' + dxContinent + '-' + band);
|
||||
cell.text(count);
|
||||
cells.push({ cell, count });
|
||||
});
|
||||
});
|
||||
|
||||
const counts = cells.map(function(c) { return c.count; });
|
||||
const min = Math.min(...counts);
|
||||
const max = Math.max(...counts);
|
||||
const range = max - min;
|
||||
cells.forEach(function({ cell, count }) {
|
||||
const t = range > 0 ? (count - min) / range : 0;
|
||||
const cls = t === 0 ? 'table-danger' : t < 0.05 ? 'table-warning' : 'table-success';
|
||||
cell.removeClass('table-danger table-warning table-success').addClass(cls);
|
||||
});
|
||||
}
|
||||
|
||||
// Called when the DE continent select changes
|
||||
function dxStatsContientChanged() {
|
||||
saveSettings();
|
||||
renderDxStats();
|
||||
}
|
||||
|
||||
// Fetch DX stats from the API and render
|
||||
function loadDxStats() {
|
||||
$.getJSON('/api/v1/dxstats', function(jsonData) {
|
||||
dxStatsData = jsonData;
|
||||
renderDxStats();
|
||||
});
|
||||
}
|
||||
|
||||
// Startup
|
||||
$(document).ready(function() {
|
||||
loadSettings();
|
||||
loadSolarConditions();
|
||||
loadDxStats();
|
||||
});
|
||||
Reference in New Issue
Block a user