import logging from datetime import datetime import requests from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout from core.constants import HTTP_HEADERS from data.sig_ref import SIGRef from data.spot import Spot from providers.spot.http_spot_provider import HTTPSpotProvider logger = logging.getLogger(__name__) class SOTA(HTTPSpotProvider): """Spot provider for Summits on the Air""" POLL_INTERVAL_SEC = 120 # SOTA wants us to check for an "epoch" from the API and see if it's actually changed before querying the main data # APIs. So it's actually the EPOCH_URL that we pass into the constructor and get the superclass to call on a timer. # The actual data lookup all happens after parsing and checking the epoch. EPOCH_URL = "https://api-db2.sota.org.uk/api/spots/epoch" SPOTS_URL = "https://api-db2.sota.org.uk/api/spots/60/all/all" SUBMIT_URL = "https://api-db2.sota.org.uk/api/spots" VALID_MODES = ["AM", "CW", "Data", "DV", "FM", "SSB"] def __init__(self, provider_config): super().__init__("SOTA", provider_config, self.EPOCH_URL, self.POLL_INTERVAL_SEC) self._api_epoch = "" def _http_response_to_spots(self, http_response): # OK, source data is actually just the epoch at this point. We'll then go on to fetch real data if we know this # has changed. epoch_changed = http_response.text != self._api_epoch self._api_epoch = http_response.text new_spots = [] # OK, if the epoch actually changed, now we make the real request for data. if epoch_changed: try: source_data = requests.get(self.SPOTS_URL, headers=HTTP_HEADERS, timeout=(5, 30)).json() # Iterate through source data for source_spot in source_data: # Convert to our spot format spot = Spot( source=self.name, source_id=source_spot["id"], dx_call=source_spot["activatorCallsign"].upper(), dx_name=source_spot["activatorName"], de_call=source_spot["callsign"].upper(), freq=(float(source_spot["frequency"]) * 1000000) if (source_spot["frequency"] is not None) else None, # Seen SOTA spots with no frequency! mode=source_spot["mode"].upper(), comment=source_spot["comments"], sig="SOTA", sig_refs=[ SIGRef( id=source_spot["summitCode"], sig="SOTA", name=source_spot["summitName"], latitude=source_spot["latitude"], longitude=source_spot["longitude"], activation_score=source_spot["points"], ) ], dx_latitude=source_spot["latitude"], dx_longitude=source_spot["longitude"], time=datetime.fromisoformat(source_spot["timeStamp"].replace("Z", "+00:00")).timestamp(), ) # Add to our list. Don't worry about de-duping, removing old spots etc. at this point; other code will do # that for us. new_spots.append(spot) except ConnectionError: logger.warning("Connection error when accessing SOTA spots API") except (ConnectTimeout, ReadTimeout): logger.warning("Timeout when accessing SOTA spots API.") return new_spots def can_submit_spot(self, sig): return sig == "SOTA" def submit_spot(self, spot, credentials): # TODO test this method works access_token = credentials.get("access_token", "") id_token = credentials.get("id_token", "") if not access_token or not id_token: raise ValueError("SOTA API tokens are required. Please log into SOTA in order to spot to it.") sig_ref = spot.sig_refs[0].id if spot.sig_refs else "" if sig_ref: # Split reference into association and summit codes ref_split = sig_ref.split("/") # Figure out a valid mode. Borrowed this from PoLo :) # https://github.com/ham2k/app-polo/blob/main/src/extensions/activities/sota/SOTAPostSelfSpot.js mode = spot.mode if mode and mode not in self.VALID_MODES: mode = "Data" body = { "activatorCallsign": spot.dx_call, "associationCode": ref_split[0], "summitCode": ref_split[1], "frequency": spot.freq / 1000000.0, "mode": mode or "", "callsign": spot.de_call, "comments": spot.comment or "", "type": "TEST", # todo replatce with NORMAL/QRT once testing complete } headers = { **HTTP_HEADERS, "Authorization": f"bearer {access_token}", "id_token": id_token, "Content-Type": "application/json", } response = requests.post(self.SUBMIT_URL, json=body, headers=headers, timeout=(5, 30)) if not response.ok: raise RuntimeError(f"SOTA API returned {response.status_code!s}: {response.text}") else: raise RuntimeError("Summit reference is required for submitting SOTA spots.")