Starting to implement alerts #17

This commit is contained in:
Ian Renton
2025-10-04 18:09:54 +01:00
parent 55893949b8
commit 74153a9d94
29 changed files with 552 additions and 109 deletions

View File

@@ -0,0 +1,43 @@
from datetime import datetime
import pytz
from core.config import SERVER_OWNER_CALLSIGN, MAX_ALERT_AGE
from core.constants import SOFTWARE_NAME, SOFTWARE_VERSION
# Generic alert provider class. Subclasses of this query the individual APIs for alerts.
class AlertProvider:
# HTTP headers used for spot providers that use HTTP
HTTP_HEADERS = { "User-Agent": SOFTWARE_NAME + " " + SOFTWARE_VERSION + " (operated by " + SERVER_OWNER_CALLSIGN + ")" }
# Constructor
def __init__(self, provider_config):
self.name = provider_config["name"]
self.enabled = provider_config["enabled"]
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
self.status = "Not Started" if self.enabled else "Disabled"
self.alerts = None
# Set up the provider, e.g. giving it the alert list to work from
def setup(self, alerts):
self.alerts = alerts
# Start the provider. This should return immediately after spawning threads to access the remote resources
def start(self):
raise NotImplementedError("Subclasses must implement this method")
# Submit a batch of alerts retrieved from the provider. There is no timestamp checking like there is for spots,
# because alerts could be created at any point for any time in the future. Rely on hashcode-based id matching
# to deal with duplicates.
def submit_batch(self, alerts):
for alert in alerts:
# Fill in any blanks
alert.infer_missing()
# Add to the list
self.alerts.add(alert.id, alert, expire=MAX_ALERT_AGE)
# Stop any threads and prepare for application shutdown
def stop(self):
raise NotImplementedError("Subclasses must implement this method")

View File

@@ -0,0 +1,61 @@
import logging
from datetime import datetime
from threading import Timer, Thread
from time import sleep
import pytz
import requests
from alertproviders.alert_provider import AlertProvider
# Generic alert provider class for providers that request data via HTTP(S). Just for convenience to avoid code
# duplication. Subclasses of this query the individual APIs for data.
class HTTPAlertProvider(AlertProvider):
def __init__(self, provider_config, url, poll_interval):
super().__init__(provider_config)
self.url = url
self.poll_interval = poll_interval
self.poll_timer = None
def start(self):
# Fire off a one-shot thread to run poll() for the first time, just to ensure start() returns immediately and
# the application can continue starting. The thread itself will then die, and the timer will kick in on its own
# thread.
logging.info("Set up query of " + self.name + " alert API every " + str(self.poll_interval) + " seconds.")
thread = Thread(target=self.poll)
thread.daemon = True
thread.start()
def stop(self):
self.poll_timer.cancel()
def poll(self):
try:
# Request data from API
logging.debug("Polling " + self.name + " alert API...")
http_response = requests.get(self.url, headers=self.HTTP_HEADERS)
# Pass off to the subclass for processing
new_alerts = self.http_response_to_alerts(http_response)
# Submit the new alerts for processing. There might not be any alerts for the less popular programs.
if new_alerts:
self.submit_batch(new_alerts)
self.status = "OK"
self.last_update_time = datetime.now(pytz.UTC)
logging.debug("Received data from " + self.name + " alert API.")
except Exception as e:
self.status = "Error"
logging.exception("Exception in HTTP JSON Alert Provider (" + self.name + ")")
sleep(1)
self.poll_timer = Timer(self.poll_interval, self.poll)
self.poll_timer.start()
# Convert an HTTP response returned by the API into alert data. The whole response is provided here so the subclass
# implementations can check for HTTP status codes if necessary, and handle the response as JSON, XML, text, whatever
# the API actually provides.
def http_response_to_alerts(self, http_response):
raise NotImplementedError("Subclasses must implement this method")

39
alertproviders/pota.py Normal file
View File

@@ -0,0 +1,39 @@
from datetime import datetime
import pytz
from alertproviders.http_alert_provider import HTTPAlertProvider
from data.alert import Alert
# Alert provider for Parks on the Air
class POTA(HTTPAlertProvider):
POLL_INTERVAL_SEC = 3600
ALERTS_URL = "https://api.pota.app/activation"
def __init__(self, provider_config):
super().__init__(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():
# Convert to our alert format
alert = Alert(source=self.name,
source_id=source_alert["scheduledActivitiesId"],
dx_call=source_alert["activator"].upper(),
freqs_modes=source_alert["frequencies"],
comment=source_alert["comments"],
sig="POTA",
sig_refs=[source_alert["reference"]],
sig_refs_names=[source_alert["name"]],
icon="tree",
start_time=datetime.strptime(source_alert["startDate"] + source_alert["startTime"],
"%Y-%m-%d%H:%M").replace(tzinfo=pytz.UTC).timestamp(),
end_time=datetime.strptime(source_alert["endDate"] + source_alert["endTime"],
"%Y-%m-%d%H:%M").replace(tzinfo=pytz.UTC).timestamp())
# Add to our list. Don't worry about de-duping, removing old alerts etc. at this point; other code will do
# that for us.
new_alerts.append(alert)
return new_alerts