From cc816d8662121f7cb67544be6ebe45c6dcb23cee Mon Sep 17 00:00:00 2001 From: Ian Renton Date: Fri, 10 Oct 2025 11:37:59 +0100 Subject: [PATCH] Allow alerts to have more than one DX callsign, and parse the "as CALL" fields from NG3K. Not yet working #38 --- alertproviders/ng3k.py | 18 ++++++++++++++---- alertproviders/pota.py | 2 +- alertproviders/sota.py | 2 +- alertproviders/wwff.py | 2 +- data/alert.py | 32 ++++++++++++++++---------------- webassets/apidocs/openapi.yml | 20 ++++++++++++-------- webassets/js/alerts.js | 3 ++- 7 files changed, 47 insertions(+), 32 deletions(-) diff --git a/alertproviders/ng3k.py b/alertproviders/ng3k.py index 69f8c58..3210989 100644 --- a/alertproviders/ng3k.py +++ b/alertproviders/ng3k.py @@ -1,3 +1,4 @@ +import re from datetime import datetime import pytz @@ -11,6 +12,7 @@ from data.alert import Alert class NG3K(HTTPAlertProvider): POLL_INTERVAL_SEC = 3600 ALERTS_URL = "https://www.ng3k.com/adxo.xml" + AS_CALL_PATTERN = re.compile("as ([a-z0-9/]+)", re.IGNORECASE) def __init__(self, provider_config): super().__init__(provider_config, self.ALERTS_URL, self.POLL_INTERVAL_SEC) @@ -49,18 +51,26 @@ class NG3K(HTTPAlertProvider): end_timestamp = datetime.strptime(end_year + " " + end_mon + " " + end_day + " 23:59", "%Y %b %d %H:%M").replace( tzinfo=pytz.UTC).timestamp() - dx_country = parts[1] - dx_call = parts[2] - qsl_info = parts[3] + # Sometimes the DX callsign is "real", sometimes you just get a prefix with the real working callsigns being + # provided in the "by" field. e.g. call="JW", by="By LA7XK as JW7XK, LA6VM as JW6VM, LA9DL as JW9DL". So + # if there are "as" callsigns in the "by" field, we extract them and replace the main call with them. + # TODO this doesn't work yet, debug extra_parts = parts[5].split("; ") by = extra_parts[0] + dx_calls = [parts[2].upper()] + match = self.AS_CALL_PATTERN.match(by) + if match: + dx_calls = [x.group(1) for x in re.finditer(self.AS_CALL_PATTERN, by)] + + dx_country = parts[1] + qsl_info = parts[3] bands = extra_parts[1] modes = extra_parts[2] if len(extra_parts) > 3 else "" comment = extra_parts[-1] # Convert to our alert format alert = Alert(source=self.name, - dx_call=dx_call.upper(), + dx_calls=dx_calls, dx_country=dx_country, freqs_modes=bands + (("; " + modes) if modes != "" else ""), comment=by + "; " + comment + "; " + qsl_info, diff --git a/alertproviders/pota.py b/alertproviders/pota.py index e748634..bc1c2c9 100644 --- a/alertproviders/pota.py +++ b/alertproviders/pota.py @@ -21,7 +21,7 @@ class POTA(HTTPAlertProvider): # Convert to our alert format alert = Alert(source=self.name, source_id=source_alert["scheduledActivitiesId"], - dx_call=source_alert["activator"].upper(), + dx_calls=[source_alert["activator"].upper()], freqs_modes=source_alert["frequencies"], comment=source_alert["comments"], sig="POTA", diff --git a/alertproviders/sota.py b/alertproviders/sota.py index 336eff2..81b7a8a 100644 --- a/alertproviders/sota.py +++ b/alertproviders/sota.py @@ -21,7 +21,7 @@ class SOTA(HTTPAlertProvider): # Convert to our alert format alert = Alert(source=self.name, source_id=source_alert["id"], - dx_call=source_alert["activatingCallsign"].upper(), + dx_calls=[source_alert["activatingCallsign"].upper()], dx_name=source_alert["activatorName"].upper(), freqs_modes=source_alert["frequency"], comment=source_alert["comments"], diff --git a/alertproviders/wwff.py b/alertproviders/wwff.py index 22d481c..14dab7f 100644 --- a/alertproviders/wwff.py +++ b/alertproviders/wwff.py @@ -21,7 +21,7 @@ class WWFF(HTTPAlertProvider): # Convert to our alert format alert = Alert(source=self.name, source_id=source_alert["id"], - dx_call=source_alert["activator_call"].upper(), + dx_calls=[source_alert["activator_call"].upper()], freqs_modes=source_alert["band"] + " " + source_alert["mode"], comment=source_alert["remarks"], sig="WWFF", diff --git a/data/alert.py b/data/alert.py index dee977f..ec66a3b 100644 --- a/data/alert.py +++ b/data/alert.py @@ -17,10 +17,10 @@ from core.utils import infer_continent_from_callsign, \ class Alert: # Unique identifier for the alert id: str = None - # Callsign of the operator that has been alertted - dx_call: str = None - # Name of the operator that has been alertted - dx_name: str = None + # Callsigns of the operators that has been alerted + dx_calls: list = None + # Names of the operators that has been alerted + dx_names: list = None # Country of the DX operator dx_country: str = None # Country flag of the DX operator @@ -88,24 +88,24 @@ class Alert: self.received_time_iso = datetime.fromtimestamp(self.received_time, pytz.UTC).isoformat() # DX country, continent, zones etc. from callsign - if self.dx_call and not self.dx_country: - self.dx_country = infer_country_from_callsign(self.dx_call) - if self.dx_call and not self.dx_continent: - self.dx_continent = infer_continent_from_callsign(self.dx_call) - if self.dx_call and not self.dx_cq_zone: - self.dx_cq_zone = infer_cq_zone_from_callsign(self.dx_call) - if self.dx_call and not self.dx_itu_zone: - self.dx_itu_zone = infer_itu_zone_from_callsign(self.dx_call) - if self.dx_call and not self.dx_dxcc_id: - self.dx_dxcc_id = infer_dxcc_id_from_callsign(self.dx_call) + if self.dx_calls and self.dx_calls[0] and not self.dx_country: + self.dx_country = infer_country_from_callsign(self.dx_calls[0]) + if self.dx_calls and self.dx_calls[0] and not self.dx_continent: + self.dx_continent = infer_continent_from_callsign(self.dx_calls[0]) + if self.dx_calls and self.dx_calls[0] and not self.dx_cq_zone: + self.dx_cq_zone = infer_cq_zone_from_callsign(self.dx_calls[0]) + if self.dx_calls and self.dx_calls[0] and not self.dx_itu_zone: + self.dx_itu_zone = infer_itu_zone_from_callsign(self.dx_calls[0]) + if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id: + self.dx_dxcc_id = infer_dxcc_id_from_callsign(self.dx_calls[0]) if self.dx_dxcc_id and not self.dx_flag: self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id] # DX operator details lookup, using QRZ.com. This should be the last resort compared to taking the data from # the actual alertting service, e.g. we don't want to accidentally use a user's QRZ.com home lat/lon instead of # the one from the park reference they're at. - if self.dx_call and not self.dx_name: - self.dx_name = infer_name_from_callsign(self.dx_call) + if self.dx_calls and not self.dx_names: + self.dx_names = list(map(lambda c: infer_name_from_callsign(c), self.dx_calls)) # Always create an ID based on a hash of every parameter *except* received_time. This is used as the index # to a map, which as a byproduct avoids us having multiple duplicate copies of the object that are identical diff --git a/webassets/apidocs/openapi.yml b/webassets/apidocs/openapi.yml index d179822..4597d34 100644 --- a/webassets/apidocs/openapi.yml +++ b/webassets/apidocs/openapi.yml @@ -708,17 +708,21 @@ components: type: string description: Unique identifier based on a hash of the alert to distinguish this one from any others. example: 442c5d56ac467341f1943e8596685073b38f5a5d4c3802ca1e16ecf98967956c - dx_call: - type: string - description: Callsign of the operator that is going to be activating - example: M0TRT + dx_calls: + type: array + description: Callsigns of the operator(s) that are going to be activating + items: + type: string + example: M0TRT dx_name: - type: string - description: Name of the operator that is going to be activating - example: Ian + type: array + description: Names of the operator(s) that are going to be activating + items: + type: string + example: Ian dx_country: type: string - description: Country of the DX operator + description: Country of the DX operator. This, and the subsequent fields, assume that all activators will be in the same country! example: United Kingdom dx_flag: type: string diff --git a/webassets/js/alerts.js b/webassets/js/alerts.js index 258296c..3d4525b 100644 --- a/webassets/js/alerts.js +++ b/webassets/js/alerts.js @@ -171,7 +171,8 @@ function addAlertRowsToTable(tbody, alerts) { // Populate the row $tr.append(`${start_time_formatted}`); $tr.append(`${end_time_formatted}`); - $tr.append(`${dx_flag}${a["dx_call"]}`); + // TODO take a["dx_calls"] and add a callsign to the following td for every one in the list. + $tr.append(`${dx_flag}${call}`); $tr.append(`${freqsModesText}`); $tr.append(`${commentText}`); $tr.append(` ${sigSourceText}`);