mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 14:27:42 +00:00
Compare commits
5
Commits
3b575de36a
...
8f8ff46426
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f8ff46426 | ||
|
|
2e51d5fdb2 | ||
|
|
3bab3cb784 | ||
|
|
a4245ed5ce | ||
|
|
64bb8ef3eb |
+9
-6
@@ -162,6 +162,9 @@ alert_providers:
|
|||||||
- class: "BOTA"
|
- class: "BOTA"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
- class: "Hamsat"
|
||||||
|
enabled: true
|
||||||
|
|
||||||
- class: "NG3K"
|
- class: "NG3K"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
@@ -308,27 +311,27 @@ callsign_data_providers:
|
|||||||
priority: 2
|
priority: 2
|
||||||
# No server-side credentials for HamQTH. Users must provide their own.
|
# No server-side credentials for HamQTH. Users must provide their own.
|
||||||
|
|
||||||
|
- class: "CountryFiles"
|
||||||
|
priority: 3
|
||||||
|
enabled: true
|
||||||
|
|
||||||
- class: "ClublogAPI"
|
- class: "ClublogAPI"
|
||||||
# Querying the Clublog API directly doesn't provide any more data than the XML version, it just provides slightly
|
# Querying the Clublog API directly doesn't provide any more data than the XML version, it just provides slightly
|
||||||
# more up-to-date information in the rare case that the prefix data changes, at a significant cost of looking up
|
# more up-to-date information in the rare case that the prefix data changes, at a significant cost of looking up
|
||||||
# every callsign via an API call. Normally left disabled but it exists as an option.
|
# every callsign via an API call. Normally left disabled but it exists as an option.
|
||||||
enabled: false
|
enabled: false
|
||||||
priority: 3
|
priority: 4
|
||||||
# API key for Clublog to look up information. Required in order to enable this provider. Unlike QRZ and HamQTH,
|
# API key for Clublog to look up information. Required in order to enable this provider. Unlike QRZ and HamQTH,
|
||||||
# Clublog uses an API key issued to Spothole, not to the end user.
|
# Clublog uses an API key issued to Spothole, not to the end user.
|
||||||
api_key: ""
|
api_key: ""
|
||||||
|
|
||||||
- class: "ClublogXML"
|
- class: "ClublogXML"
|
||||||
enabled: true
|
enabled: true
|
||||||
priority: 4
|
priority: 5
|
||||||
# API key for Clublog to look up information. Required in order to enable this provider. You will need to request
|
# API key for Clublog to look up information. Required in order to enable this provider. You will need to request
|
||||||
# one via their helpdesk portal if you want to use callsign lookups from Clublog.
|
# one via their helpdesk portal if you want to use callsign lookups from Clublog.
|
||||||
api_key: ""
|
api_key: ""
|
||||||
|
|
||||||
- class: "CountryFiles"
|
|
||||||
priority: 5
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
|
|
||||||
# Maximum time to keep spots and alerts in the system before deleting them. By default, one hour for spots and one week
|
# Maximum time to keep spots and alerts in the system before deleting them. By default, one hour for spots and one week
|
||||||
# for alerts.
|
# for alerts.
|
||||||
|
|||||||
@@ -12,6 +12,22 @@ HAMQTH_PRG = f"Spothole v{SOFTWARE_VERSION} operated by {SERVER_OWNER_CALLSIGN}"
|
|||||||
|
|
||||||
# Special Interest Groups
|
# Special Interest Groups
|
||||||
SIGS = [
|
SIGS = [
|
||||||
|
SIG(
|
||||||
|
name="AMSAT",
|
||||||
|
comment_names=[],
|
||||||
|
description="Amateur Radio Satellites",
|
||||||
|
sig_type=SIGType.WORLDWIDE,
|
||||||
|
icon="fa-satellite",
|
||||||
|
refs_globally_unique=False,
|
||||||
|
),
|
||||||
|
SIG(
|
||||||
|
name="EME",
|
||||||
|
comment_names=[],
|
||||||
|
description="Moonbounce",
|
||||||
|
sig_type=SIGType.WORLDWIDE,
|
||||||
|
icon="fa-moon",
|
||||||
|
refs_globally_unique=False,
|
||||||
|
),
|
||||||
SIG(
|
SIG(
|
||||||
name="POTA",
|
name="POTA",
|
||||||
comment_names=["POTA"],
|
comment_names=["POTA"],
|
||||||
|
|||||||
+1
-3
@@ -71,7 +71,6 @@ class ModeSource(str, Enum):
|
|||||||
SPOT = "SPOT"
|
SPOT = "SPOT"
|
||||||
COMMENT = "COMMENT"
|
COMMENT = "COMMENT"
|
||||||
BANDPLAN = "BANDPLAN"
|
BANDPLAN = "BANDPLAN"
|
||||||
NONE = "NONE"
|
|
||||||
|
|
||||||
|
|
||||||
class LocationSourceForSpot(str, Enum):
|
class LocationSourceForSpot(str, Enum):
|
||||||
@@ -82,7 +81,6 @@ class LocationSourceForSpot(str, Enum):
|
|||||||
GRID = "GRID"
|
GRID = "GRID"
|
||||||
HOME_QTH = "HOME QTH"
|
HOME_QTH = "HOME QTH"
|
||||||
DXCC = "DXCC"
|
DXCC = "DXCC"
|
||||||
NONE = "NONE"
|
|
||||||
|
|
||||||
|
|
||||||
class LocationSourceForCallsign(str, Enum):
|
class LocationSourceForCallsign(str, Enum):
|
||||||
@@ -90,7 +88,6 @@ class LocationSourceForCallsign(str, Enum):
|
|||||||
|
|
||||||
HOME_QTH = "HOME QTH"
|
HOME_QTH = "HOME QTH"
|
||||||
DXCC = "DXCC"
|
DXCC = "DXCC"
|
||||||
NONE = "NONE"
|
|
||||||
|
|
||||||
|
|
||||||
class SIGRefType(str, Enum):
|
class SIGRefType(str, Enum):
|
||||||
@@ -119,6 +116,7 @@ class AlertType(str, Enum):
|
|||||||
"""Type of an alert."""
|
"""Type of an alert."""
|
||||||
|
|
||||||
XOTA = "XOTA"
|
XOTA = "XOTA"
|
||||||
|
SATELLITE = "SATELLITE"
|
||||||
DXPEDITION = "DXPEDITION"
|
DXPEDITION = "DXPEDITION"
|
||||||
CONTEST = "CONTEST"
|
CONTEST = "CONTEST"
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,9 @@ def get_sig_ref_info(sig_name, ref_id):
|
|||||||
# database.
|
# database.
|
||||||
if sig_name.upper() == "DME":
|
if sig_name.upper() == "DME":
|
||||||
match = re.match(r"DME[\- ](\d{3,5})", ref_id, re.IGNORECASE)
|
match = re.match(r"DME[\- ](\d{3,5})", ref_id, re.IGNORECASE)
|
||||||
number = match.group(1)
|
if match:
|
||||||
ref_id = f"DME-{number.zfill(5)}"
|
number = match.group(1)
|
||||||
|
ref_id = f"DME-{number.zfill(5)}"
|
||||||
|
|
||||||
# DTMBA spotters sometimes include spaces and dashes, our regex allows them but they must be removed here so we
|
# DTMBA spotters sometimes include spaces and dashes, our regex allows them but they must be removed here so we
|
||||||
# can look up against the official list which doesn't have them
|
# can look up against the official list which doesn't have them
|
||||||
|
|||||||
+7
-5
@@ -1,7 +1,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
@@ -64,7 +64,7 @@ class Alert:
|
|||||||
# Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
# Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
||||||
sig: str | None = None
|
sig: str | None = None
|
||||||
# SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
|
# SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
|
||||||
sig_refs: list | None = None
|
sig_refs: list = field(default_factory=list)
|
||||||
|
|
||||||
# Timing info
|
# Timing info
|
||||||
|
|
||||||
@@ -128,13 +128,13 @@ class Alert:
|
|||||||
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
|
# Fetch SIG 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
|
# 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 a SIG even though there's no real lookup, just maths
|
# from WAB and WAI, which count as a SIG even though there's no real lookup, just maths
|
||||||
if self.sig_refs and len(self.sig_refs) > 0:
|
if self.sig_refs:
|
||||||
for sig_ref in self.sig_refs:
|
for sig_ref in self.sig_refs:
|
||||||
populate_missing_sig_ref_info(sig_ref)
|
populate_missing_sig_ref_info(sig_ref)
|
||||||
|
|
||||||
# If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG
|
# If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG
|
||||||
# and apply it to the whole spot.
|
# and apply it to the whole spot.
|
||||||
if self.sig_refs and len(self.sig_refs) > 0 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
|
||||||
|
|
||||||
# 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
|
||||||
@@ -158,7 +158,9 @@ class Alert:
|
|||||||
self.icon = "fa-globe-africa"
|
self.icon = "fa-globe-africa"
|
||||||
elif self.alert_type == AlertType.CONTEST:
|
elif self.alert_type == AlertType.CONTEST:
|
||||||
self.icon = "fa-trophy"
|
self.icon = "fa-trophy"
|
||||||
elif self.sig_refs and len(self.sig_refs) > 0 and self.sig_refs[0].icon:
|
elif self.alert_type == AlertType.CONTEST:
|
||||||
|
self.icon = "fa-satellite"
|
||||||
|
elif self.sig_refs and self.sig_refs[0].icon:
|
||||||
self.icon = self.sig_refs[0].icon
|
self.icon = self.sig_refs[0].icon
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ class Callsign:
|
|||||||
# ITU zone in which the callsign indicates they are operating
|
# ITU zone in which the callsign indicates they are operating
|
||||||
itu_zone: int | None = None
|
itu_zone: int | None = None
|
||||||
# Location source
|
# Location source
|
||||||
location_source: LocationSourceForCallsign = LocationSourceForCallsign.NONE
|
location_source: LocationSourceForCallsign | None = None
|
||||||
|
|
||||||
def fully_populated(self):
|
def fully_populated(self):
|
||||||
"""Utility method to indicate that the callsign data is fully populated. Multiple providers can return data for
|
"""Utility method to indicate that the callsign data is fully populated. Multiple providers can return data for
|
||||||
|
|||||||
+2
-2
@@ -8,10 +8,10 @@ class SIGRef:
|
|||||||
"""Data class that defines a Special Interest Group "info" or reference. As well as the basic reference ID we include a
|
"""Data class that defines a Special Interest Group "info" or reference. As well as the basic reference ID we include a
|
||||||
name and a lookup URL."""
|
name and a lookup URL."""
|
||||||
|
|
||||||
# Reference ID, e.g. "GB-0001".
|
|
||||||
id: str
|
|
||||||
# SIG that this reference is in, e.g. "POTA".
|
# SIG that this reference is in, e.g. "POTA".
|
||||||
sig: str
|
sig: str
|
||||||
|
# Reference ID, e.g. "GB-0001".
|
||||||
|
id: str | None = None
|
||||||
# Name of the reference, e.g. "Null Country Park", if known.
|
# Name of the reference, e.g. "Null Country Park", if known.
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
# Type of the reference, e.g. "Park", if known.
|
# Type of the reference, e.g. "Park", if known.
|
||||||
|
|||||||
+22
-14
@@ -2,7 +2,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from math import isnan
|
from math import isnan
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class Spot:
|
|||||||
dx_latitude: float | None = None
|
dx_latitude: float | None = None
|
||||||
dx_longitude: float | None = None
|
dx_longitude: float | None = None
|
||||||
# DX Location source. Indicates how accurate the location might be.
|
# DX Location source. Indicates how accurate the location might be.
|
||||||
dx_location_source: LocationSourceForSpot = LocationSourceForSpot.NONE
|
dx_location_source: LocationSourceForSpot | None = None
|
||||||
# DX Location good. Indicates that the software thinks the location data is good enough to plot on a map. This is
|
# DX Location good. Indicates that the software thinks the location data is good enough to plot on a map. This is
|
||||||
# true if the location source is "SPOT", "SIG REF LOOKUP" or "GRID", or if the location source is "HOME QTH" and the
|
# true if the location source is "SPOT", "SIG REF LOOKUP" or "GRID", or if the location source is "HOME QTH" and the
|
||||||
# DX callsign doesn't have a suffix like /P.
|
# DX callsign doesn't have a suffix like /P.
|
||||||
@@ -107,7 +107,7 @@ class Spot:
|
|||||||
# Inferred mode "family".
|
# Inferred mode "family".
|
||||||
mode_type: ModeType | None = None
|
mode_type: ModeType | None = None
|
||||||
# Source of the mode information.
|
# Source of the mode information.
|
||||||
mode_source: ModeSource = ModeSource.NONE
|
mode_source: ModeSource | None = None
|
||||||
# Frequency, in Hz
|
# Frequency, in Hz
|
||||||
freq: float | None = None
|
freq: float | None = None
|
||||||
# Band, defined by the frequency, e.g. "40m" or "70cm"
|
# Band, defined by the frequency, e.g. "40m" or "70cm"
|
||||||
@@ -124,7 +124,7 @@ class Spot:
|
|||||||
# Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
# Special Interest Group (SIG), e.g. outdoor activity programme such as POTA
|
||||||
sig: str | None = None
|
sig: str | None = None
|
||||||
# SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
|
# SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO
|
||||||
sig_refs: list | None = None
|
sig_refs: list = field(default_factory=list)
|
||||||
|
|
||||||
# Timing info
|
# Timing info
|
||||||
|
|
||||||
@@ -262,12 +262,12 @@ class Spot:
|
|||||||
self.dx_location_source = LocationSourceForSpot.SPOT
|
self.dx_location_source = LocationSourceForSpot.SPOT
|
||||||
|
|
||||||
# Set the top-level "SIG" if it is missing but we have at least one SIG ref.
|
# Set the top-level "SIG" if it is missing but we have at least one SIG ref.
|
||||||
if not self.sig and self.sig_refs and len(self.sig_refs) > 0:
|
if not self.sig and self.sig_refs:
|
||||||
self.sig = self.sig_refs[0].sig.upper()
|
self.sig = self.sig_refs[0].sig.upper()
|
||||||
|
|
||||||
# See if we already have a SIG reference, but the comment looks like it contains more for the same SIG. This
|
# See if we already have a SIG reference, but the comment looks like it contains more for the same SIG. This
|
||||||
# should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
|
# should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
|
||||||
if self.comment and self.sig_refs and len(self.sig_refs) > 0 and self.sig_refs[0].sig:
|
if self.comment and self.sig_refs and self.sig_refs[0].sig:
|
||||||
sig = self.sig_refs[0].sig.upper()
|
sig = self.sig_refs[0].sig.upper()
|
||||||
regex = get_ref_regex_for_sig(sig)
|
regex = get_ref_regex_for_sig(sig)
|
||||||
if regex:
|
if regex:
|
||||||
@@ -315,7 +315,7 @@ class Spot:
|
|||||||
# Fetch SIG data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a reference
|
# Fetch SIG 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
|
# 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 a SIG even though there's no real lookup, just maths
|
# from WAB and WAI, which count as a SIG even though there's no real lookup, just maths
|
||||||
if self.sig_refs and len(self.sig_refs) > 0:
|
if self.sig_refs:
|
||||||
for sig_ref in self.sig_refs:
|
for sig_ref in self.sig_refs:
|
||||||
sig_ref = populate_missing_sig_ref_info(sig_ref)
|
sig_ref = populate_missing_sig_ref_info(sig_ref)
|
||||||
# If the spot itself doesn't have location yet, but the SIG ref does, extract it
|
# If the spot itself doesn't have location yet, but the SIG ref does, extract it
|
||||||
@@ -331,7 +331,7 @@ class Spot:
|
|||||||
|
|
||||||
# If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG
|
# If the spot itself doesn't have a SIG yet, but we have at least one SIG reference, take that reference's SIG
|
||||||
# and apply it to the whole spot.
|
# and apply it to the whole spot.
|
||||||
if self.sig_refs and len(self.sig_refs) > 0 and not self.sig:
|
if self.sig_refs and not self.sig:
|
||||||
self.sig = self.sig_refs[0].sig
|
self.sig = self.sig_refs[0].sig
|
||||||
|
|
||||||
# Parse "de_grid<prop_mode>dx_grid" structures from the comment, e.g. "JN61ES(ES)JM56XT" or "JO02GQ<>KN17LG".
|
# Parse "de_grid<prop_mode>dx_grid" structures from the comment, e.g. "JN61ES(ES)JM56XT" or "JO02GQ<>KN17LG".
|
||||||
@@ -359,6 +359,16 @@ class Spot:
|
|||||||
self.propagation_mode = mode_tag
|
self.propagation_mode = mode_tag
|
||||||
logger.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}")
|
logger.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}")
|
||||||
|
|
||||||
|
# Set SIGs based on propagation mode
|
||||||
|
if self.propagation_mode == "Satellite":
|
||||||
|
if not self.sig:
|
||||||
|
self.sig = "AMSAT"
|
||||||
|
self.sig_refs.append(SIGRef(sig="AMSAT"))
|
||||||
|
if self.propagation_mode == "Earth-Moon-Earth":
|
||||||
|
if not self.sig:
|
||||||
|
self.sig = "EME"
|
||||||
|
self.sig_refs.append(SIGRef(sig="EME"))
|
||||||
|
|
||||||
# Parse "de_grid -> dx_grid" structures from the comment
|
# Parse "de_grid -> dx_grid" structures from the comment
|
||||||
if self.comment:
|
if self.comment:
|
||||||
grid_mode_grid_match = re.search(
|
grid_mode_grid_match = re.search(
|
||||||
@@ -415,7 +425,7 @@ class Spot:
|
|||||||
|
|
||||||
# Determine a "QTH" string. If we have a SIG ref, pick the first one and turn it into a suitable string,
|
# Determine a "QTH" string. If we have a SIG ref, pick the first one and turn it into a suitable string,
|
||||||
# otherwise see what they have set on an online lookup service.
|
# otherwise see what they have set on an online lookup service.
|
||||||
if self.sig_refs and len(self.sig_refs) > 0:
|
if self.sig_refs:
|
||||||
qth = self.sig_refs[0].id
|
qth = self.sig_refs[0].id
|
||||||
if self.sig_refs[0].name:
|
if self.sig_refs[0].name:
|
||||||
qth += f" {self.sig_refs[0].name}"
|
qth += f" {self.sig_refs[0].name}"
|
||||||
@@ -462,7 +472,7 @@ class Spot:
|
|||||||
|
|
||||||
# Icon for the spot should be the icon of the first SIG ref if present, otherwise a radio tower
|
# Icon for the spot should be the icon of the first SIG ref if present, otherwise a radio tower
|
||||||
self.icon = "fa-tower-cell"
|
self.icon = "fa-tower-cell"
|
||||||
if self.sig_refs and len(self.sig_refs) > 0 and self.sig_refs[0].icon:
|
if self.sig_refs and self.sig_refs[0].icon:
|
||||||
self.icon = self.sig_refs[0].icon
|
self.icon = self.sig_refs[0].icon
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -476,16 +486,14 @@ class Spot:
|
|||||||
def _append_sig_ref_if_missing(self, new_sig_ref):
|
def _append_sig_ref_if_missing(self, new_sig_ref):
|
||||||
"""Append a sig_ref to the list, so long as it's not already there."""
|
"""Append a sig_ref to the list, so long as it's not already there."""
|
||||||
|
|
||||||
sig_refs = self.sig_refs or []
|
|
||||||
self.sig_refs = sig_refs
|
|
||||||
new_sig_ref.id = new_sig_ref.id.strip().upper()
|
new_sig_ref.id = new_sig_ref.id.strip().upper()
|
||||||
new_sig_ref.sig = new_sig_ref.sig.strip().upper()
|
new_sig_ref.sig = new_sig_ref.sig.strip().upper()
|
||||||
if new_sig_ref.id == "":
|
if new_sig_ref.id == "":
|
||||||
return
|
return
|
||||||
for sig_ref in sig_refs:
|
for sig_ref in self.sig_refs:
|
||||||
if sig_ref.id == new_sig_ref.id and sig_ref.sig == new_sig_ref.sig:
|
if sig_ref.id == new_sig_ref.id and sig_ref.sig == new_sig_ref.sig:
|
||||||
return
|
return
|
||||||
sig_refs.append(new_sig_ref)
|
self.sig_refs.append(new_sig_ref)
|
||||||
|
|
||||||
def expired(self):
|
def expired(self):
|
||||||
"""Decide if this spot has expired (in which case it should not be added to the system in the first place, and not
|
"""Decide if this spot has expired (in which case it should not be added to the system in the first place, and not
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
from core.enums import AlertType
|
||||||
|
from data.alert import Alert
|
||||||
|
from data.sig_ref import SIGRef
|
||||||
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
|
|
||||||
|
|
||||||
|
class Hamsat(HTTPAlertProvider):
|
||||||
|
"""Alert provider for Hamsat (hams.at)"""
|
||||||
|
|
||||||
|
POLL_INTERVAL_SEC = 1800
|
||||||
|
ALERTS_URL = "https://hams.at/api/alerts"
|
||||||
|
|
||||||
|
def __init__(self, provider_config):
|
||||||
|
super().__init__("Hamsat", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC)
|
||||||
|
|
||||||
|
def _http_response_to_alerts(self, http_response):
|
||||||
|
new_alerts = []
|
||||||
|
# Iterate through source data
|
||||||
|
for source_alert in http_response.json()["data"]:
|
||||||
|
# Convert to our alert format
|
||||||
|
alert = Alert(
|
||||||
|
source=self.name,
|
||||||
|
source_id=source_alert["id"],
|
||||||
|
dx_calls=[source_alert["callsign"].upper()],
|
||||||
|
freqs_modes=f"{source_alert['mhz']!s} {source_alert['mhz_direction']}, {source_alert['mode']}",
|
||||||
|
comment=source_alert["comment"],
|
||||||
|
# Fudge a SIG ref to provide the remaining bits of data we need: the satellite and the operator's grid
|
||||||
|
sig_refs=[
|
||||||
|
SIGRef(
|
||||||
|
sig="AMSAT",
|
||||||
|
id=f"{source_alert['satellite']['name']} from {source_alert['grids'][0]}",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
start_time=datetime.strptime(source_alert["aos_at"], "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
.replace(tzinfo=pytz.UTC)
|
||||||
|
.timestamp(),
|
||||||
|
end_time=datetime.strptime(source_alert["los_at"], "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
.replace(tzinfo=pytz.UTC)
|
||||||
|
.timestamp(),
|
||||||
|
alert_type=AlertType.SATELLITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add to our list
|
||||||
|
new_alerts.append(alert)
|
||||||
|
return new_alerts
|
||||||
@@ -17,7 +17,7 @@ info:
|
|||||||
|
|
||||||
### 2.1
|
### 2.1
|
||||||
|
|
||||||
* Added DTMBA, FEA, BIWOTA, COTA & PGA SIGs
|
* Added AMSAT, EME, DTMBA, FEA, BIWOTA, COTA & PGA SIGs
|
||||||
* Removed the distinction between LSB & USB (both will now show as SSB) and between the various digital voice modes, which will now show as DV.
|
* Removed the distinction between LSB & USB (both will now show as SSB) and between the various digital voice modes, which will now show as DV.
|
||||||
* Added `sig_type`, `icon`, `region_flag` and `refs_globally_unique` to SIG data
|
* Added `sig_type`, `icon`, `region_flag` and `refs_globally_unique` to SIG data
|
||||||
* Added `icon` to spot and alert data
|
* Added `icon` to spot and alert data
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
Faros de España (FEA), Diploma Muesos de España (DMUE), Diploma Castillos de España (DCE), Diploma Monumentos y
|
Faros de España (FEA), Diploma Muesos de España (DMUE), Diploma Castillos de España (DCE), Diploma Monumentos y
|
||||||
Vestigios de España (DMVE), Diploma Estaciones de Ferrocarril de España (DEFE), Diploma Teatri Musei e Belle
|
Vestigios de España (DMVE), Diploma Estaciones de Ferrocarril de España (DEFE), Diploma Teatri Musei e Belle
|
||||||
Arti (DTMBA), British Inland Waterways on the Air (BIWOTA), Castles on the Air (COTA), Polish Gmina Award (PGA),
|
Arti (DTMBA), British Inland Waterways on the Air (BIWOTA), Castles on the Air (COTA), Polish Gmina Award (PGA),
|
||||||
and Toilets on the Air.</p>
|
EME/Moonbounce, Amateur Satellite (AMSAT), and Toilets on the Air.</p>
|
||||||
<p>As of the time of writing in August 2026, I think Spothole captures most outdoor radio programmes that have a
|
<p>As of the time of writing in August 2026, I think Spothole captures most outdoor radio programmes that have a
|
||||||
defined, downloadable reference list, and almost certainly those that have a spotting/alerting API. If you know
|
defined, downloadable reference list, and almost certainly those that have a spotting/alerting API. If you know
|
||||||
of one I've missed, please let me know!</p>
|
of one I've missed, please let me know!</p>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/add-spot.js?v=1789059482"></script>
|
<script src="/static/js/add-spot.js?v=1789116473"></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>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/alerts.js?v=1789059482"></script>
|
<script src="/static/js/alerts.js?v=1789116474"></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=1789059482"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1789116473"></script>
|
||||||
<script src="/static/js/bands.js?v=1789059482"></script>
|
<script src="/static/js/bands.js?v=1789116473"></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=1789059482" type="text/css">
|
<link rel="stylesheet" href="/static/css/style.css?v=1789116473" 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=1789059482"></script>
|
<script src="/static/js/utils.js?v=1789116473"></script>
|
||||||
<script src="/static/js/ui-ham.js?v=1789059482"></script>
|
<script src="/static/js/ui-ham.js?v=1789116473"></script>
|
||||||
<script src="/static/js/geo.js?v=1789059482"></script>
|
<script src="/static/js/geo.js?v=1789116473"></script>
|
||||||
<script src="/static/js/common.js?v=1789059482"></script>
|
<script src="/static/js/common.js?v=1789116473"></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=1789059482"></script>
|
<script src="/static/js/conditions.js?v=1789116473"></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=1789059482"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1789116474"></script>
|
||||||
<script src="/static/js/map.js?v=1789059482"></script>
|
<script src="/static/js/map.js?v=1789116474"></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>
|
||||||
|
|||||||
@@ -113,8 +113,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/spotsbandsandmap.js?v=1789059482"></script>
|
<script src="/static/js/spotsbandsandmap.js?v=1789116473"></script>
|
||||||
<script src="/static/js/spots.js?v=1789059482"></script>
|
<script src="/static/js/spots.js?v=1789116473"></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>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/status.js?v=1789059482"></script>
|
<script src="/static/js/status.js?v=1789116473"></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