mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-24 16:24:32 +00:00
Improve the way hamsat alerts work, and add the ability to detect satellite names from spot comments. Closes #149
This commit is contained in:
@@ -52,11 +52,11 @@ def get_activity_ref_info(activity_name, ref_id):
|
|||||||
if activity_name.upper() == ActivityName.DTMBA:
|
if activity_name.upper() == ActivityName.DTMBA:
|
||||||
ref_id = ref_id.replace("-", "").replace(" ", "")
|
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.
|
# 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
|
return activity_ref
|
||||||
|
|
||||||
### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ###
|
### PROGRAMMATIC DATA GENERATION INSTEAD OF LOOKUPS ###
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ class ActivityRefType(str, Enum):
|
|||||||
BUILDING = "BUILDING"
|
BUILDING = "BUILDING"
|
||||||
REGION = "REGION"
|
REGION = "REGION"
|
||||||
GRID = "GRID"
|
GRID = "GRID"
|
||||||
|
SATELLITE = "SATELLITE"
|
||||||
TOILET = "TOILET"
|
TOILET = "TOILET"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -20,9 +20,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
sig_type=ActivityType.TRADITIONAL,
|
sig_type=ActivityType.TRADITIONAL,
|
||||||
has_refs=False,
|
has_refs=False,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
# DXpedition stations are never really spotted with "DXpedition" in the comments, but we can assign
|
comment_names=["DXPEDITION"],
|
||||||
# this activity to a spot other ways.
|
|
||||||
comment_names=[],
|
|
||||||
icon="fa-book-atlas",
|
icon="fa-book-atlas",
|
||||||
alerts_possible=True,
|
alerts_possible=True,
|
||||||
),
|
),
|
||||||
@@ -30,8 +28,12 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
name=ActivityName.SATELLITE,
|
name=ActivityName.SATELLITE,
|
||||||
description="Amateur Radio Satellite",
|
description="Amateur Radio Satellite",
|
||||||
sig_type=ActivityType.TRADITIONAL,
|
sig_type=ActivityType.TRADITIONAL,
|
||||||
has_refs=False,
|
# Satellite "references" are the names of the satellites themselves. This is not an exhaustive list, it just
|
||||||
refs_globally_unique=False,
|
# 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=[],
|
comment_names=[],
|
||||||
icon="fa-satellite",
|
icon="fa-satellite",
|
||||||
alerts_possible=True,
|
alerts_possible=True,
|
||||||
|
|||||||
+34
-6
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pytz
|
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_lookup_helper import populate_missing_activity_ref_info
|
||||||
from core.activity_utils import get_icon_for_activity
|
from core.activity_utils import get_icon_for_activity
|
||||||
@@ -40,6 +41,11 @@ class Alert:
|
|||||||
dx_cq_zone: int | None = None
|
dx_cq_zone: int | None = None
|
||||||
# ITU zone of the DX operator
|
# ITU zone of the DX operator
|
||||||
dx_itu_zone: int | None = None
|
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
|
# General alert info
|
||||||
|
|
||||||
@@ -108,8 +114,7 @@ class Alert:
|
|||||||
if self.received_time and not self.received_time_iso:
|
if self.received_time and not self.received_time_iso:
|
||||||
self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat()
|
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
|
# DX country, continent, zones etc. from callsign.
|
||||||
# have a real location for alerts.
|
|
||||||
if self.dx_calls and self.dx_calls[0]:
|
if self.dx_calls and self.dx_calls[0]:
|
||||||
call_info = get_call_info(self.dx_calls[0], credentials)
|
call_info = get_call_info(self.dx_calls[0], credentials)
|
||||||
if self.dx_calls and self.dx_calls[0] and not self.dx_country:
|
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:
|
if self.dx_dxcc_id and not self.dx_flag:
|
||||||
self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id)
|
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
|
# Fetch activity data, and set a real position if we can get one.
|
||||||
# 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
|
|
||||||
if self.sig_refs:
|
if self.sig_refs:
|
||||||
for activity_ref in 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
|
# 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.
|
# reference's activity and apply it to the whole spot.
|
||||||
if self.sig_refs and self.sig_refs[0] and not self.sig:
|
if self.sig_refs and self.sig_refs[0] and not self.sig:
|
||||||
self.sig = self.sig_refs[0].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
|
# 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
|
# 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.
|
# user updates of their alert comments without duplicating in the system.
|
||||||
|
|||||||
@@ -22,17 +22,20 @@ class Hamsat(HTTPAlertProvider):
|
|||||||
# Iterate through source data
|
# Iterate through source data
|
||||||
for source_alert in http_response.json()["data"]:
|
for source_alert in http_response.json()["data"]:
|
||||||
# Convert to our alert format
|
# Convert to our alert format
|
||||||
freqs_modes = source_alert.get("mode", "")
|
freqs_modes = source_alert.get("mode") or ""
|
||||||
if "mhz" in source_alert:
|
mhz = source_alert.get("mhz")
|
||||||
if "mhz_direction" in source_alert:
|
if mhz is not None:
|
||||||
freqs_modes = f"{source_alert['mhz']!s} {source_alert['mhz_direction']}, {freqs_modes}"
|
mhz_direction = source_alert.get("mhz_direction")
|
||||||
|
if mhz_direction is not None:
|
||||||
|
freqs_modes = f"{mhz!s} {mhz_direction}, {freqs_modes}"
|
||||||
else:
|
else:
|
||||||
freqs_modes = f"{source_alert['mhz']!s}, {freqs_modes}"
|
freqs_modes = f"{mhz!s}, {freqs_modes}"
|
||||||
|
|
||||||
alert = Alert(
|
alert = Alert(
|
||||||
source=self.name,
|
source=self.name,
|
||||||
source_id=source_alert["id"],
|
source_id=source_alert["id"],
|
||||||
dx_calls=[source_alert["callsign"].upper()],
|
dx_calls=[source_alert["callsign"].upper()],
|
||||||
|
dx_grid=source_alert["grids"][0],
|
||||||
freqs_modes=freqs_modes,
|
freqs_modes=freqs_modes,
|
||||||
comment=source_alert["comment"],
|
comment=source_alert["comment"],
|
||||||
sig=ActivityName.SATELLITE,
|
sig=ActivityName.SATELLITE,
|
||||||
@@ -40,7 +43,7 @@ class Hamsat(HTTPAlertProvider):
|
|||||||
sig_refs=[
|
sig_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=ActivityName.SATELLITE,
|
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")
|
start_time=datetime.strptime(source_alert["aos_at"], "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|||||||
@@ -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 :)
|
* 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 QRP, RaDAR Rally, /AM and /MM activities
|
||||||
* Added `has_refs` and `alerts_possible` to Activity data
|
* Added `has_refs` and `alerts_possible` to Activity data
|
||||||
|
* Added `dx_grid`, `dx_latitude` and `dx_longitude` to alert data
|
||||||
|
|
||||||
### 2.1
|
### 2.1
|
||||||
|
|
||||||
@@ -1452,6 +1453,21 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
description: ITU zone of the DX operator
|
description: ITU zone of the DX operator
|
||||||
example: 14
|
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:
|
freqs_modes:
|
||||||
type: string
|
type: string
|
||||||
description: An indication of the frequencies and modes that the activation will use, if provided.
|
description: An indication of the frequencies and modes that the activation will use, if provided.
|
||||||
|
|||||||
@@ -266,6 +266,11 @@ function addAlertRowsToTable(tbody, alerts) {
|
|||||||
} else {
|
} else {
|
||||||
items[i] = `${escapeHtml(a["sig_refs"][i]["id"])}`
|
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(", ");
|
activityRefs = items.join(", ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
</div>
|
</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 () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-add-spot").addClass("active");
|
$("#nav-link-add-spot").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/alerts.js?v=1790196142"></script>
|
<script src="/static/js/alerts.js?v=1790197717"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-alerts").addClass("active");
|
$("#nav-link-alerts").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -76,8 +76,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/spotsbandsandmap.js?v=1790196142"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1790197716"></script>
|
||||||
<script src="/static/js/bands.js?v=1790196142"></script>
|
<script src="/static/js/bands.js?v=1790197716"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-bands").addClass("active");
|
$("#nav-link-bands").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
{% extends "skeleton.html" %}
|
{% extends "skeleton.html" %}
|
||||||
{% block head_extra %}
|
{% 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/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/fontawesome-6.7.2.min.css" rel="stylesheet">
|
||||||
<link href="/static/vendor/css/solid-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;
|
window.fetchEventSource = fetchEventSource;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/static/js/utils.js?v=1790196142"></script>
|
<script src="/static/js/utils.js?v=1790197716"></script>
|
||||||
<script src="/static/js/ui-ham.js?v=1790196142"></script>
|
<script src="/static/js/ui-ham.js?v=1790197716"></script>
|
||||||
<script src="/static/js/geo.js?v=1790196142"></script>
|
<script src="/static/js/geo.js?v=1790197716"></script>
|
||||||
<script src="/static/js/common.js?v=1790196142"></script>
|
<script src="/static/js/common.js?v=1790197716"></script>
|
||||||
{% end %}
|
{% end %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
|
<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 () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-conditions").addClass("active");
|
$("#nav-link-conditions").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
+2
-2
@@ -113,8 +113,8 @@
|
|||||||
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
|
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/static/js/spotsbandsandmap.js?v=1790196142"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1790197717"></script>
|
||||||
<script src="/static/js/map.js?v=1790196142"></script>
|
<script src="/static/js/map.js?v=1790197717"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-map").addClass("active");
|
$("#nav-link-map").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -125,8 +125,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/spotsbandsandmap.js?v=1790196142"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1790197716"></script>
|
||||||
<script src="/static/js/spots.js?v=1790196142"></script>
|
<script src="/static/js/spots.js?v=1790197716"></script>
|
||||||
<script>$(document).ready(function () {
|
<script>$(document).ready(function () {
|
||||||
$("#nav-link-spots").addClass("active");
|
$("#nav-link-spots").addClass("active");
|
||||||
}); <!-- highlight active page in nav --></script>
|
}); <!-- highlight active page in nav --></script>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/status.js?v=1790196142"></script>
|
<script src="/static/js/status.js?v=1790197716"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$("#nav-link-status").addClass("active");
|
$("#nav-link-status").addClass("active");
|
||||||
|
|||||||
Reference in New Issue
Block a user