mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-20 06:17:41 +00:00
Add support for WA7BNM Contest Calendar. Closes #106
This commit is contained in:
@@ -165,6 +165,9 @@ alert_providers:
|
||||
- class: "NG3K"
|
||||
enabled: true
|
||||
|
||||
- class: "WA7BNM"
|
||||
enabled: true
|
||||
|
||||
- class: "RSGBHFContests"
|
||||
enabled: true
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
from datetime import datetime, time
|
||||
from typing import cast
|
||||
|
||||
import pytz
|
||||
from icalendar import Calendar, Event
|
||||
|
||||
from data.alert import Alert
|
||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||
|
||||
|
||||
class ICALAlertProvider(HTTPAlertProvider):
|
||||
"""Generic alert provider for iCal calendars. Defines an abstract method event_to_alert(event) that subclasses must
|
||||
implement, and use it to convert an iCal event to an Alert object based on whatever format their iCal events use."""
|
||||
|
||||
def __init__(self, name, provider_config, url, poll_interval):
|
||||
super().__init__(name, provider_config, url, poll_interval)
|
||||
|
||||
def _http_response_to_alerts(self, http_response):
|
||||
new_alerts = []
|
||||
cal = Calendar.from_ical(http_response.content)
|
||||
|
||||
# Iterate through events, passing each one in turn to the subclass' event_to_alert method to turn it into
|
||||
# a Spothole alert object
|
||||
for component in cal.walk():
|
||||
if component.name != "VEVENT":
|
||||
continue
|
||||
|
||||
event = cast(Event, component)
|
||||
alert = self.event_to_alert(event)
|
||||
new_alerts.append(alert)
|
||||
return new_alerts
|
||||
|
||||
def event_to_alert(self, event: Event) -> Alert:
|
||||
"""Convert an ICal event to an Alert object. Subclasses must implement this method."""
|
||||
|
||||
@staticmethod
|
||||
def _to_utc_timestamp(value):
|
||||
"""Convert a date or datetime value from an iCal field into a UTC UNIX timestamp."""
|
||||
|
||||
# Datetime object so we can treat it as-is, check if it has a non-UTC tz and convert it if necessary
|
||||
if isinstance(value, datetime):
|
||||
if value.tzinfo is None:
|
||||
value = pytz.UTC.localize(value)
|
||||
return value.astimezone(pytz.UTC).timestamp()
|
||||
|
||||
# Date object so this is an all day event
|
||||
return pytz.UTC.localize(datetime.combine(value, time.min)).timestamp()
|
||||
@@ -1,101 +1,75 @@
|
||||
import re
|
||||
from datetime import datetime, time
|
||||
from typing import cast
|
||||
|
||||
import pytz
|
||||
from icalendar import Calendar, Event
|
||||
from icalendar import Event
|
||||
|
||||
from core.enums import AlertType, Continent
|
||||
from data.alert import Alert
|
||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||
from providers.alert.ical_alert_provider import ICALAlertProvider
|
||||
|
||||
|
||||
class RSGBICALAlertProvider(HTTPAlertProvider):
|
||||
"""Generic alert provider for RSGB contest ICAL calendars."""
|
||||
class RSGBICALAlertProvider(ICALAlertProvider):
|
||||
"""Generic alert provider for RSGB contest iCal calendars. Builds on the generic iCal alert provider by adding
|
||||
handling specific to how RSGB's iCal events are formatted. This is still effectively an abstract class itself;
|
||||
RSGB has two contest calendars (HF & VHF) that each subclass this."""
|
||||
|
||||
def __init__(self, name, provider_config, url, poll_interval):
|
||||
super().__init__(name, provider_config, url, poll_interval)
|
||||
|
||||
FREQ_PATTERN = re.compile(r"([\d.]+(?:MHz|GHz))|SHF")
|
||||
|
||||
def _http_response_to_alerts(self, http_response):
|
||||
new_alerts = []
|
||||
cal = Calendar.from_ical(http_response.content)
|
||||
def event_to_alert(self, event: Event) -> Alert:
|
||||
"""Convert an iCal event in RSGB's format to an Alert object."""
|
||||
|
||||
# Iterate through events
|
||||
for component in cal.walk():
|
||||
if component.name != "VEVENT":
|
||||
continue
|
||||
summary = str(event.get("summary", "")).strip()
|
||||
|
||||
event = cast(Event, component)
|
||||
summary = str(event.get("summary", "")).strip()
|
||||
# Ensure summaries start with "RSGB" to avoid any confusion
|
||||
if not summary.startswith("RSGB "):
|
||||
summary = "RSGB " + summary
|
||||
|
||||
# Ensure summaries start with "RSGB" to avoid any confusion
|
||||
if not summary.startswith("RSGB "):
|
||||
summary = "RSGB " + summary
|
||||
# Extract freq from summary. HF contests don't give frequencies, all VHF ones do
|
||||
match = self.FREQ_PATTERN.search(summary)
|
||||
if match:
|
||||
freqs_modes = match.group()
|
||||
else:
|
||||
freqs_modes = "HF bands"
|
||||
freqs_modes = freqs_modes + ", "
|
||||
|
||||
# Extract freq from summary
|
||||
freqs_modes = ""
|
||||
# Extract mode from summary
|
||||
if "FMAC" in summary:
|
||||
freqs_modes = freqs_modes + "FM"
|
||||
elif "CW" in summary:
|
||||
freqs_modes = freqs_modes + "CW"
|
||||
elif "SSB" in summary:
|
||||
freqs_modes = freqs_modes + "SSB"
|
||||
elif "FT8" in summary:
|
||||
freqs_modes = freqs_modes + "FT8"
|
||||
elif "FT4" in summary:
|
||||
freqs_modes = freqs_modes + "FT4"
|
||||
elif "DATA" in summary:
|
||||
freqs_modes = freqs_modes + "Data modes"
|
||||
else:
|
||||
freqs_modes = freqs_modes + "All modes"
|
||||
|
||||
# HF contests don't give frequencies, all VHF ones do
|
||||
match = self.FREQ_PATTERN.search(summary)
|
||||
if match:
|
||||
freqs_modes = match.group()
|
||||
else:
|
||||
freqs_modes = "HF bands"
|
||||
freqs_modes = freqs_modes + ", "
|
||||
dtstart = event.get("dtstart")
|
||||
dtend = event.get("dtend")
|
||||
start_timestamp = self._to_utc_timestamp(dtstart.dt)
|
||||
end_timestamp = self._to_utc_timestamp(dtend.dt) - 1 if dtend is not None else start_timestamp
|
||||
|
||||
# Extract mode from summary
|
||||
if "FMAC" in summary:
|
||||
freqs_modes = freqs_modes + "FM"
|
||||
elif "CW" in summary:
|
||||
freqs_modes = freqs_modes + "CW"
|
||||
elif "SSB" in summary:
|
||||
freqs_modes = freqs_modes + "SSB"
|
||||
elif "FT8" in summary:
|
||||
freqs_modes = freqs_modes + "FT8"
|
||||
elif "FT4" in summary:
|
||||
freqs_modes = freqs_modes + "FT4"
|
||||
elif "DATA" in summary:
|
||||
freqs_modes = freqs_modes + "Data modes"
|
||||
else:
|
||||
freqs_modes = freqs_modes + "All modes"
|
||||
# Convert to our alert format
|
||||
alert = Alert(
|
||||
source=self.name,
|
||||
dx_calls=[],
|
||||
dx_country="United Kingdom",
|
||||
dx_dxcc_id=235,
|
||||
dx_continent=Continent.EU,
|
||||
dx_cq_zone=14,
|
||||
dx_itu_zone=27,
|
||||
dx_flag="🇬🇧",
|
||||
freqs_modes=freqs_modes,
|
||||
comment=summary,
|
||||
start_time=start_timestamp,
|
||||
end_time=end_timestamp,
|
||||
alert_type=AlertType.CONTEST,
|
||||
)
|
||||
|
||||
dtstart = event.get("dtstart")
|
||||
dtend = event.get("dtend")
|
||||
start_timestamp = self._to_utc_timestamp(dtstart.dt)
|
||||
end_timestamp = self._to_utc_timestamp(dtend.dt) - 1 if dtend is not None else start_timestamp
|
||||
|
||||
# Convert to our alert format
|
||||
alert = Alert(
|
||||
source=self.name,
|
||||
dx_calls=[],
|
||||
dx_country="United Kingdom",
|
||||
dx_dxcc_id=235,
|
||||
dx_continent=Continent.EU,
|
||||
dx_cq_zone=14,
|
||||
dx_itu_zone=27,
|
||||
dx_flag="🇬🇧",
|
||||
freqs_modes=freqs_modes,
|
||||
comment=summary,
|
||||
start_time=start_timestamp,
|
||||
end_time=end_timestamp,
|
||||
alert_type=AlertType.CONTEST,
|
||||
)
|
||||
|
||||
# Add to our list.
|
||||
new_alerts.append(alert)
|
||||
return new_alerts
|
||||
|
||||
@staticmethod
|
||||
def _to_utc_timestamp(value):
|
||||
"""Convert a date or datetime value from an iCal field into a UTC UNIX timestamp."""
|
||||
|
||||
# Datetime object so we can treat it as-is, check if it has a non-UTC tz and convert it if necessary
|
||||
if isinstance(value, datetime):
|
||||
if value.tzinfo is None:
|
||||
value = pytz.UTC.localize(value)
|
||||
return value.astimezone(pytz.UTC).timestamp()
|
||||
|
||||
# Date object so this is an all day event
|
||||
return pytz.UTC.localize(datetime.combine(value, time.min)).timestamp()
|
||||
return alert
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from icalendar import Event
|
||||
|
||||
from core.enums import AlertType
|
||||
from data.alert import Alert
|
||||
from providers.alert.ical_alert_provider import ICALAlertProvider
|
||||
|
||||
|
||||
class WA7BNM(ICALAlertProvider):
|
||||
"""Alert provider for the WA7BNM contest calendar."""
|
||||
|
||||
POLL_INTERVAL_DAYS = 1
|
||||
ALERTS_URL = "https://contestcalendar.com/weeklycontcustom.php"
|
||||
|
||||
def __init__(self, provider_config):
|
||||
super().__init__(
|
||||
"WA7BNM Contest Calendar", provider_config, self.ALERTS_URL, self.POLL_INTERVAL_DAYS * 24 * 60 * 60
|
||||
)
|
||||
|
||||
def event_to_alert(self, event: Event) -> Alert:
|
||||
"""Convert an iCal event in WA7BNM's format to an Alert object."""
|
||||
|
||||
summary = str(event.get("summary", ""))
|
||||
url = str(event.get("url", ""))
|
||||
|
||||
dtstart = event.get("dtstart")
|
||||
dtend = event.get("dtend")
|
||||
start_timestamp = self._to_utc_timestamp(dtstart.dt)
|
||||
end_timestamp = self._to_utc_timestamp(dtend.dt) - 1 if dtend is not None else start_timestamp
|
||||
|
||||
# Convert to our alert format
|
||||
alert = Alert(
|
||||
source=self.name,
|
||||
dx_calls=[],
|
||||
comment=summary,
|
||||
url=url,
|
||||
start_time=start_timestamp,
|
||||
end_time=end_timestamp,
|
||||
alert_type=AlertType.CONTEST,
|
||||
)
|
||||
|
||||
return alert
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
pyyaml~=6.0.3
|
||||
requests-cache~=1.2.1
|
||||
pyhamtools~=0.12.0
|
||||
pyhamtools~=0.13.2
|
||||
telnetlib3~=2.0.8
|
||||
pytz~=2025.2
|
||||
requests~=2.32.4
|
||||
|
||||
+1
-1
@@ -233,7 +233,7 @@ function addAlertRowsToTable(tbody, alerts) {
|
||||
commentText = escapeHtml(a["comment"]);
|
||||
}
|
||||
if (a["url"] != null) {
|
||||
commentText = `<a href="${escapeHtml(a['url'])}" target="_new">${commentText}</a>`;
|
||||
commentText += ` <a href="${escapeHtml(a['url'])}" target="_new" style="text-decoration: none">🔗</a>`;
|
||||
}
|
||||
|
||||
// Sig or fallback to source
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/add-spot.js?v=1789023274"></script>
|
||||
<script src="/static/js/add-spot.js?v=1789055140"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-add-spot").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/alerts.js?v=1789023274"></script>
|
||||
<script src="/static/js/alerts.js?v=1789055140"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-alerts").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1789023274"></script>
|
||||
<script src="/static/js/bands.js?v=1789023274"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1789055140"></script>
|
||||
<script src="/static/js/bands.js?v=1789055140"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-bands").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{% extends "skeleton.html" %}
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1789023274" type="text/css">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=1789055140" 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=1789023274"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1789023274"></script>
|
||||
<script src="/static/js/geo.js?v=1789023274"></script>
|
||||
<script src="/static/js/common.js?v=1789023274"></script>
|
||||
<script src="/static/js/utils.js?v=1789055140"></script>
|
||||
<script src="/static/js/ui-ham.js?v=1789055140"></script>
|
||||
<script src="/static/js/geo.js?v=1789055140"></script>
|
||||
<script src="/static/js/common.js?v=1789055140"></script>
|
||||
{% end %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
|
||||
@@ -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=1789023274"></script>
|
||||
<script src="/static/js/conditions.js?v=1789055140"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-conditions").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
+2
-2
@@ -113,8 +113,8 @@
|
||||
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
|
||||
</script>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1789023274"></script>
|
||||
<script src="/static/js/map.js?v=1789023274"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1789055140"></script>
|
||||
<script src="/static/js/map.js?v=1789055140"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-map").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -113,8 +113,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1789023274"></script>
|
||||
<script src="/static/js/spots.js?v=1789023274"></script>
|
||||
<script src="/static/js/spotsbandsandmap.js?v=1789055140"></script>
|
||||
<script src="/static/js/spots.js?v=1789055140"></script>
|
||||
<script>$(document).ready(function () {
|
||||
$("#nav-link-spots").addClass("active");
|
||||
}); <!-- highlight active page in nav --></script>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/status.js?v=1789023274"></script>
|
||||
<script src="/static/js/status.js?v=1789055140"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#nav-link-status").addClass("active");
|
||||
|
||||
Reference in New Issue
Block a user