Improve the way hamsat alerts work, and add the ability to detect satellite names from spot comments. Closes #149

This commit is contained in:
Ian Renton
2026-09-23 22:08:36 +01:00
parent 981fea57ed
commit c26482f06b
15 changed files with 90 additions and 35 deletions
+3 -3
View File
@@ -52,11 +52,11 @@ def get_activity_ref_info(activity_name, ref_id):
if activity_name.upper() == ActivityName.DTMBA:
ref_id = ref_id.replace("-", "").replace(" ", "")
### NO DATA ACTIVITIES ###
### NO REFERENCE ACTIVITIES ###
#
# If the activity is HEMA or BIWOTA, we have no way to either generate useful data or look it up on a
# If the activity doesn't have references, we have no way to either generate useful data or look it up on a
# reference list, so just skip the lookup here.
if activity_name.upper() == ActivityName.HEMA or activity_name.upper() == ActivityName.BIWOTA:
if not activity.has_refs:
return activity_ref
### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ###
+1
View File
@@ -163,6 +163,7 @@ class ActivityRefType(str, Enum):
BUILDING = "BUILDING"
REGION = "REGION"
GRID = "GRID"
SATELLITE = "SATELLITE"
TOILET = "TOILET"
+7 -5
View File
@@ -20,9 +20,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
sig_type=ActivityType.TRADITIONAL,
has_refs=False,
refs_globally_unique=False,
# DXpedition stations are never really spotted with "DXpedition" in the comments, but we can assign
# this activity to a spot other ways.
comment_names=[],
comment_names=["DXPEDITION"],
icon="fa-book-atlas",
alerts_possible=True,
),
@@ -30,8 +28,12 @@ ACTIVITIES: dict[ActivityName, Activity] = {
name=ActivityName.SATELLITE,
description="Amateur Radio Satellite",
sig_type=ActivityType.TRADITIONAL,
has_refs=False,
refs_globally_unique=False,
# Satellite "references" are the names of the satellites themselves. This is not an exhaustive list, it just
# matches some of the most commonly used amateur radio satellites so they can be picked out of spot comments.
has_refs=True,
refs_globally_unique=True,
ref_type=ActivityRefType.SATELLITE,
ref_regex=r"ISS|AO-(?:7|27|73|91|95|123)|QO-100|QO100|QO 100|RS-44|SO-50",
comment_names=[],
icon="fa-satellite",
alerts_possible=True,
+34 -6
View File
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
from datetime import datetime, timedelta
import pytz
from pyhamtools.locator import locator_to_latlong, latlong_to_locator
from core.activity_lookup_helper import populate_missing_activity_ref_info
from core.activity_utils import get_icon_for_activity
@@ -40,6 +41,11 @@ class Alert:
dx_cq_zone: int | None = None
# ITU zone of the DX operator
dx_itu_zone: int | None = None
# Maidenhead grid locator for the DX. This could be from a geographical reference e.g. POTA or grid.
dx_grid: str | None = None
# Latitude & longitude of the DX, in degrees. This could be from a geographical reference e.g. POTA or grid.
dx_latitude: float | None = None
dx_longitude: float | None = None
# General alert info
@@ -108,8 +114,7 @@ class Alert:
if self.received_time and not self.received_time_iso:
self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat()
# DX country, continent, zones etc. from callsign. CQ/ITU zone are better looked up with a location but we don't
# have a real location for alerts.
# DX country, continent, zones etc. from callsign.
if self.dx_calls and self.dx_calls[0]:
call_info = get_call_info(self.dx_calls[0], credentials)
if self.dx_calls and self.dx_calls[0] and not self.dx_country:
@@ -125,18 +130,41 @@ class Alert:
if self.dx_dxcc_id and not self.dx_flag:
self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id)
# Fetch activity data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a
# reference in its initial call, we use this code to populate the rest of the data. This includes working
# out grid refs from WAB and WAI, which count as an activity even though there's no real lookup, just maths
# Fetch activity data, and set a real position if we can get one.
if self.sig_refs:
for activity_ref in self.sig_refs:
populate_missing_activity_ref_info(activity_ref)
activity_ref = populate_missing_activity_ref_info(activity_ref)
# If the alert itself doesn't have location yet, but the activity ref does, extract it
if activity_ref.grid and not self.dx_grid:
self.dx_grid = activity_ref.grid
if (
activity_ref.latitude
and not self.dx_latitude
and activity_ref.longitude
and not self.dx_longitude
):
self.dx_latitude = activity_ref.latitude
self.dx_longitude = activity_ref.longitude
# If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that
# reference's activity and apply it to the whole spot.
if self.sig_refs and self.sig_refs[0] and not self.sig:
self.sig = self.sig_refs[0].sig
# DX Grid to lat/lon and vice versa in case one is missing
if self.dx_grid and (not self.dx_latitude or not self.dx_longitude):
try:
ll = locator_to_latlong(self.dx_grid)
self.dx_latitude = ll[0]
self.dx_longitude = ll[1]
except Exception:
logger.debug("Invalid grid received for spot", exc_info=True)
if self.dx_latitude and self.dx_longitude and not self.dx_grid:
try:
self.dx_grid = latlong_to_locator(self.dx_latitude, self.dx_longitude, 8)
except Exception:
logger.debug("Invalid lat/lon received for spot", exc_info=True)
# Create an ID based on the source and source ID if possible, as these guaranee uniqueness. If there is no
# source ID, use a combination of callsign and start time. Excluding things like the comment here allows for
# user updates of their alert comments without duplicating in the system.
+9 -6
View File
@@ -22,17 +22,20 @@ class Hamsat(HTTPAlertProvider):
# Iterate through source data
for source_alert in http_response.json()["data"]:
# Convert to our alert format
freqs_modes = source_alert.get("mode", "")
if "mhz" in source_alert:
if "mhz_direction" in source_alert:
freqs_modes = f"{source_alert['mhz']!s} {source_alert['mhz_direction']}, {freqs_modes}"
freqs_modes = source_alert.get("mode") or ""
mhz = source_alert.get("mhz")
if mhz is not None:
mhz_direction = source_alert.get("mhz_direction")
if mhz_direction is not None:
freqs_modes = f"{mhz!s} {mhz_direction}, {freqs_modes}"
else:
freqs_modes = f"{source_alert['mhz']!s}, {freqs_modes}"
freqs_modes = f"{mhz!s}, {freqs_modes}"
alert = Alert(
source=self.name,
source_id=source_alert["id"],
dx_calls=[source_alert["callsign"].upper()],
dx_grid=source_alert["grids"][0],
freqs_modes=freqs_modes,
comment=source_alert["comment"],
sig=ActivityName.SATELLITE,
@@ -40,7 +43,7 @@ class Hamsat(HTTPAlertProvider):
sig_refs=[
ActivityRef(
sig=ActivityName.SATELLITE,
id=f"{source_alert['satellite']['name']} from {source_alert['grids'][0]}",
id=source_alert["satellite"]["name"],
)
],
start_time=datetime.strptime(source_alert["aos_at"], "%Y-%m-%dT%H:%M:%SZ")
+16
View File
@@ -21,6 +21,7 @@ info:
* Removed `alert_type` from alert data. Contest, DXpedition and Satellite alerts now give those values in `sig` instead, alongside the existing outdoor activity programmes. Teeeeechnically a breaking change but AlertType is so new I doubt anyone is using it yet, so slipped this one in anyway. Sorry :)
* Added QRP, RaDAR Rally, /AM and /MM activities
* Added `has_refs` and `alerts_possible` to Activity data
* Added `dx_grid`, `dx_latitude` and `dx_longitude` to alert data
### 2.1
@@ -1452,6 +1453,21 @@ components:
type: integer
description: ITU zone of the DX operator
example: 14
dx_grid:
type: string
description: >
Maidenhead grid locator for the activator's location, if known.
example: IO91aa
dx_latitude:
type: number
description: >
Latitude of the activator's location, if known, in degrees.
example: 51.2345
dx_longitude:
type: number
description: >
Longitude of the activator's location, if known, in degrees.
example: -1.2345
freqs_modes:
type: string
description: An indication of the frequencies and modes that the activation will use, if provided.
+5
View File
@@ -266,6 +266,11 @@ function addAlertRowsToTable(tbody, alerts) {
} else {
items[i] = `${escapeHtml(a["sig_refs"][i]["id"])}`
}
// If this is a satellite alert the ref will just be the satellite, but DX grid is also important, so
// show that if we can.
if (a["sig_refs"][i]["sig"] === "Satellite" && a["dx_grid"]) {
items[i] += " from " + a["sig_refs"][i]["dx_grid"];
}
}
activityRefs = items.join(", ");
}
+1 -1
View File
@@ -77,7 +77,7 @@
</div>
<script src="/static/js/add-spot.js?v=1790196142"></script>
<script src="/static/js/add-spot.js?v=1790197716"></script>
<script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -85,7 +85,7 @@
</div>
<script src="/static/js/alerts.js?v=1790196142"></script>
<script src="/static/js/alerts.js?v=1790197717"></script>
<script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -76,8 +76,8 @@
</div>
<script src="/static/js/spotsbandsandmap.js?v=1790196142"></script>
<script src="/static/js/bands.js?v=1790196142"></script>
<script src="/static/js/spotsbandsandmap.js?v=1790197716"></script>
<script src="/static/js/bands.js?v=1790197716"></script>
<script>$(document).ready(function () {
$("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script>
+5 -5
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %}
{% block head_extra %}
<link rel="stylesheet" href="/static/css/style.css?v=1790196142" type="text/css">
<link rel="stylesheet" href="/static/css/style.css?v=1790197716" type="text/css">
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -16,10 +16,10 @@
window.fetchEventSource = fetchEventSource;
</script>
<script src="/static/js/utils.js?v=1790196142"></script>
<script src="/static/js/ui-ham.js?v=1790196142"></script>
<script src="/static/js/geo.js?v=1790196142"></script>
<script src="/static/js/common.js?v=1790196142"></script>
<script src="/static/js/utils.js?v=1790197716"></script>
<script src="/static/js/ui-ham.js?v=1790197716"></script>
<script src="/static/js/geo.js?v=1790197716"></script>
<script src="/static/js/common.js?v=1790197716"></script>
{% end %}
{% block body %}
<div class="container">
+1 -1
View File
@@ -284,7 +284,7 @@
</div>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/static/js/conditions.js?v=1790196142"></script>
<script src="/static/js/conditions.js?v=1790197716"></script>
<script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -113,8 +113,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
</script>
<script src="/static/js/spotsbandsandmap.js?v=1790196142"></script>
<script src="/static/js/map.js?v=1790196142"></script>
<script src="/static/js/spotsbandsandmap.js?v=1790197717"></script>
<script src="/static/js/map.js?v=1790197717"></script>
<script>$(document).ready(function () {
$("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -125,8 +125,8 @@
</div>
<script src="/static/js/spotsbandsandmap.js?v=1790196142"></script>
<script src="/static/js/spots.js?v=1790196142"></script>
<script src="/static/js/spotsbandsandmap.js?v=1790197716"></script>
<script src="/static/js/spots.js?v=1790197716"></script>
<script>$(document).ready(function () {
$("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -96,7 +96,7 @@
</div>
</div>
<script src="/static/js/status.js?v=1790196142"></script>
<script src="/static/js/status.js?v=1790197716"></script>
<script>
$(document).ready(function () {
$("#nav-link-status").addClass("active");