Files
spothole/providers/spot/gma.py
T

179 lines
9.2 KiB
Python

import logging
from datetime import datetime
import pytz
from core.constants import HTTP_HEADERS
from core.enums import ActivityName, ActivityRefType, Mode
from core.url_data_cache import URLDataCache
from data.activity_ref import ActivityRef
from data.spot import Spot
from providers.spot.http_spot_provider import HTTPSpotProvider
logger = logging.getLogger(__name__)
class GMA(HTTPSpotProvider):
"""Spot provider for General Mountain Activity"""
POLL_INTERVAL_SEC = 120
SPOTS_URL = "https://www.gma.rocks/api/spots/25/"
# GMA spots don't contain the details of the programme they are for, we need a separate lookup for that
REF_INFO_URL_ROOT = "https://www.gma.rocks/api/ref/?"
def __init__(self, provider_config):
# Ensure there is an API key in our config, and set up the query URL using it. If no key is provided,
# disable this spot provider.
self._api_key = provider_config.get("api_key", "")
if self._api_key == "":
provider_config["enabled"] = False
logger.warning("GMA spot provider configured but no api key was provided, this API will not be queried.")
self._url_data_cache = URLDataCache("GMA")
super().__init__(
"GMA",
provider_config,
f"{self.SPOTS_URL}?key={self._api_key}",
self.POLL_INTERVAL_SEC,
)
def _http_response_to_spots(self, http_response):
new_spots = []
# Iterate through source data
if "RCD" in http_response.json():
for source_spot in http_response.json()["RCD"]:
# Convert to our spot format
# Seen GMA spots with no (or empty) lat/lon
lat = float(source_spot["LAT"]) if (source_spot["LAT"] and source_spot["LAT"] != "") else None
lon = float(source_spot["LON"]) if (source_spot["LON"] and source_spot["LON"] != "") else None
# Seen some real janky times from GMA, if we don't understand it just ignore this spot
try:
time = (
datetime.strptime(source_spot["DATE"] + source_spot["TIME"], "%Y%m%d%H%M")
.replace(tzinfo=pytz.UTC)
.timestamp()
)
except ValueError:
continue
spot = Spot(
source=self.name,
dx_call=source_spot["ACTIVATOR"].upper(),
de_call=source_spot["SPOTTER"].upper(),
# Seen GMA spots with no frequency or with "QRT" in this field
freq=float(source_spot["QRG"]) * 1000
if (source_spot["QRG"] != "" and source_spot["QRG"] != "QRT")
else None,
# Filter out some weird mode strings
mode=Mode.from_name(source_spot["MODE"].upper()) if "<>" not in source_spot["MODE"] else None,
comment=source_spot["TEXT"],
sig_refs=[
ActivityRef(
id=source_spot["REF"],
sig="",
name=source_spot["NAME"],
latitude=lat,
longitude=lon,
)
],
time=time,
dx_latitude=lat,
dx_longitude=lon,
qrt=source_spot["QRG"] == "QRT",
)
# GMA doesn't give what programme (activity) the reference is for until we separately look it up.
if "REF" in source_spot:
try:
ref_response = self._url_data_cache.get(
self.REF_INFO_URL_ROOT + source_spot["REF"],
headers=HTTP_HEADERS,
)
# Sometimes this is blank even if it's a 200 response, so handle that
if (
ref_response.ok
and ref_response.text is not None
and ref_response.text != ""
and ref_response.text != "\n"
):
ref_info = ref_response.json()
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. POTA and WWFF
# spots come through with reftype=POTA or reftype=WWFF. SOTA is harder to figure out because both SOTA
# and GMA summits come through with reftype=Summit, so we must check for the presence of a "sota" entry
# to determine if it's a SOTA summit.
if (
spot.sig_refs
and ref_info
and "reftype" in ref_info
and ref_info["reftype"] not in [ActivityName.POTA, ActivityName.WWFF]
and (
ref_info["reftype"] != "Summit" or "sota" not in ref_info or ref_info["sota"] == ""
)
):
match ref_info["reftype"]:
case "Summit":
spot.sig_refs[0].sig = ActivityName.GMA
spot.sig_refs[0].ref_type = ActivityRefType.SUMMIT
spot.sig = ActivityName.GMA
case "IOTA Island":
spot.sig_refs[0].sig = ActivityName.IOTA
spot.sig_refs[0].ref_type = ActivityRefType.ISLAND
spot.sig = ActivityName.IOTA
case "GMA Island":
spot.sig_refs[0].sig = ActivityName.GMA_ISLANDS
spot.sig_refs[0].ref_type = ActivityRefType.ISLAND
spot.sig = ActivityName.GMA_ISLANDS
case "Lighthouse (ILLW)":
spot.sig_refs[0].sig = ActivityName.ILLW
spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
spot.sig = ActivityName.ILLW
case "Lighthouse (ARLHS)":
spot.sig_refs[0].sig = ActivityName.ARLHS
spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
spot.sig = ActivityName.ARLHS
case "Castle":
spot.sig_refs[0].sig = ActivityName.WCA
spot.sig_refs[0].ref_type = ActivityRefType.CASTLE
spot.sig = ActivityName.WCA
case "Mill":
spot.sig_refs[0].sig = ActivityName.MOTA
spot.sig_refs[0].ref_type = ActivityRefType.MILL
spot.sig = ActivityName.MOTA
case _:
logger.warning(
f"GMA spot found with ref type {ref_info['reftype']}, developer needs to add support for this!"
)
spot.sig_refs[0].sig = ref_info["reftype"]
spot.sig = ref_info["reftype"]
elif not ref_response.from_cache:
if not ref_response.ok:
logger.warning(
f"HTTP {ref_response.status_code} when looking up GMA ref {source_spot['REF']}"
)
else:
logger.debug(
f"GMA API had no data for {source_spot['REF']}, it may not be a valid reference."
)
except Exception:
logger.exception(
f"Exception when looking up {self.REF_INFO_URL_ROOT}{source_spot['REF']}, activity data will not be populated for the spot."
)
# 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)
else:
logger.warning(f"The GMA API returned an unexpected response (HTTP {http_response.status_code}).")
return new_spots
def can_submit_spot(self, activity):
return activity == ActivityName.GMA
def submit_spot(self, spot, credentials):
# TODO: Implement.
# Spotting to GMA is documented: https://www.cqgma.org/api/doc/apigma_spot.pdf We (or the user) need a GMA account, and to send the password in plaintext(!!)
raise NotImplementedError("GMA upstream spot submission is not yet implemented")