import hashlib import json from dataclasses import dataclass from datetime import datetime import copy import pytz from core.constants import DXCC_FLAGS from core.utils import infer_continent_from_callsign, \ infer_country_from_callsign, infer_cq_zone_from_callsign, infer_itu_zone_from_callsign, infer_dxcc_id_from_callsign, \ infer_name_from_callsign # Data class that defines an alert. @dataclass 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 # Country of the DX operator dx_country: str = None # Country flag of the DX operator dx_flag: str = None # Continent of the DX operator dx_continent: str = None # DXCC ID of the DX operator dx_dxcc_id: int = None # CQ zone of the DX operator dx_cq_zone: int = None # ITU zone of the DX operator dx_itu_zone: int = None # Intended frequencies & modes of operation. Essentially just a different kind of comment field. freqs_modes: str = None # Start time of the activation, UTC seconds since UNIX epoch start_time: float = None # Start time of the activation of the alert, ISO 8601 start_time_iso: str = None # End time of the activation, UTC seconds since UNIX epoch. Optional end_time: float = None # End time of the activation of the alert, ISO 8601 end_time_iso: str = None # Time that this software received the alert, UTC seconds since UNIX epoch. This is used with the "since_received" # call to our API to receive all data that is new to us, even if by a quirk of the API it might be older than the # list time the client polled the API. received_time: float = None # Time that this software received the alert, ISO 8601 received_time_iso: str = None # Comment made by the alerter, if any comment: str = None # Special Interest Group (SIG), e.g. outdoor activity programme such as POTA sig: str = None # SIG references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO sig_refs: list = None # SIG reference names sig_refs_names: list = None # Activation score. SOTA only activation_score: int = None # Icon, from the Font Awesome set. This is fairly opinionated but is here to help the alerthole web UI and Field alertter. Does not include the "fa-" prefix. icon: str = "question" # Where we got the alert from, e.g. "POTA", "SOTA"... source: str = None # The ID the source gave it, if any. source_id: str = None # Infer missing parameters where possible def infer_missing(self): # If we somehow don't have a start time, set it to zero so it sorts off the bottom of any list but # clients can still reliably parse it as a number. if not self.start_time: self.start_time = 0 # If we don't have a received time, this has just been received so set that to "now" if not self.received_time: self.received_time = datetime.now(pytz.UTC).timestamp() # Fill in ISO versions of times, in case the client prefers that if self.start_time and not self.start_time_iso: self.start_time_iso = datetime.fromtimestamp(self.start_time, pytz.UTC).isoformat() if self.end_time and not self.end_time_iso: self.end_time_iso = datetime.fromtimestamp(self.end_time, pytz.UTC).isoformat() if self.received_time and not self.received_time_iso: 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_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) # 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 # apart from that they were retrieved from the API at different times. Note that the simple Python hash() # function includes a seed randomly generated at runtime; this is therefore not consistent between runs. But we # use diskcache to store our data between runs, so we use SHA256 which does not include this random element. self_copy = copy.deepcopy(self) self_copy.received_time = 0 self_copy.received_time_iso = "" self.id = hashlib.sha256(str(self_copy).encode("utf-8")).hexdigest() # JSON serialise def to_json(self): return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True)