diff --git a/config-example.yml b/config-example.yml index 5fd2ef5..fb32f24 100644 --- a/config-example.yml +++ b/config-example.yml @@ -9,4 +9,8 @@ server-owner-callsign: "N0CALL" web-server-port: 8080 # Maximum spot age to keep in the system before deleting it -max-spot-age-sec: 3600 \ No newline at end of file +max-spot-age-sec: 3600 + +# Login for QRZ.com to look up information. Optional. +qrz-username: "N0CALL" +qrz-password: "" \ No newline at end of file diff --git a/core/utils.py b/core/utils.py index 343b58f..a352788 100644 --- a/core/utils.py +++ b/core/utils.py @@ -3,12 +3,17 @@ from datetime import datetime from pyhamtools import LookupLib, Callinfo +from core.config import config from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES -# Static lookup helpers from pyhamtools -# todo in future add QRZ as a second lookup option in case it provides more data? -lookuplib = LookupLib(lookuptype="countryfile") -callinfo = Callinfo(lookuplib) +# Lookup helpers from pyhamtools +LOOKUP_LIB_BASIC = LookupLib(lookuptype="countryfile") +CALL_INFO_BASIC = Callinfo(LOOKUP_LIB_BASIC) +QRZ_AVAILABLE = config["qrz-password"] != "" +if QRZ_AVAILABLE: + LOOKUP_LIB_QRZ = LookupLib(lookuptype="qrz", username=config["qrz-username"], pwd=config["qrz-password"]) +# Cache of QRZ.com callsign lookups, so we don't repeatedly call the API for stuff we already know +QRZ_CALLSIGN_DATA_CACHE = {} # Infer a mode from the comment def infer_mode_from_comment(comment): @@ -40,38 +45,82 @@ def infer_band_from_freq(freq): # Infer a country name from a callsign def infer_country_from_callsign(call): try: - return callinfo.get_country_name(call) + return CALL_INFO_BASIC.get_country_name(call) except KeyError as e: return None # Infer a DXCC ID from a callsign def infer_dxcc_id_from_callsign(call): try: - return callinfo.get_adif_id(call) + return CALL_INFO_BASIC.get_adif_id(call) except KeyError as e: return None # Infer a continent shortcode from a callsign def infer_continent_from_callsign(call): try: - return callinfo.get_continent(call) + return CALL_INFO_BASIC.get_continent(call) except KeyError as e: return None # Infer a CQ zone from a callsign def infer_cq_zone_from_callsign(call): try: - return callinfo.get_cqz(call) + return CALL_INFO_BASIC.get_cqz(call) except KeyError as e: return None # Infer a ITU zone from a callsign def infer_itu_zone_from_callsign(call): try: - return callinfo.get_ituz(call) + return CALL_INFO_BASIC.get_ituz(call) except KeyError as e: return None +# Utility method to get QRZ.com data from cache if possible, if not get it from the API and cache it +def get_qrz_data_for_callsign(call): + # Fetch from cache if we can, otherwise fetch from the API and cache it + if call in QRZ_CALLSIGN_DATA_CACHE: + return QRZ_CALLSIGN_DATA_CACHE[call] + elif QRZ_AVAILABLE: + try: + data = LOOKUP_LIB_QRZ.lookup_callsign(callsign=call) + QRZ_CALLSIGN_DATA_CACHE[call] = data + return data + except KeyError: + # QRZ had no info for the call, that's OK + return None + else: + return None + +# Infer an operator name from a callsign (requires QRZ.com) +def infer_name_from_callsign(call): + data = get_qrz_data_for_callsign(call) + if data and "fname" in data: + name = data["fname"] + if "name" in data: + name = name + " " + data["name"] + return name + else: + return None + +# Infer a latitude and longitude from a callsign (requires QRZ.com) +def infer_latlon_from_callsign(call): + data = get_qrz_data_for_callsign(call) + if data and "latitude" in data and "longitude" in data: + return [data["longitude"], data["longitude"]] + else: + return None + +# Infer a grid locator from a callsign (requires QRZ.com) +def infer_grid_from_callsign(call): + data = get_qrz_data_for_callsign(call) + if data and "locator" in data: + return data["locator"] + else: + return None + + # Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things. # Converts datetimes to ISO. # Anything else it tries to convert to a dict. diff --git a/data/spot.py b/data/spot.py index 80affaa..15decca 100644 --- a/data/spot.py +++ b/data/spot.py @@ -8,7 +8,7 @@ from pyhamtools.locator import locator_to_latlong, latlong_to_locator from core.constants import DXCC_FLAGS from core.utils import infer_mode_family_from_mode, infer_band_from_freq, infer_continent_from_callsign, \ infer_country_from_callsign, infer_cq_zone_from_callsign, infer_itu_zone_from_callsign, infer_dxcc_id_from_callsign, \ - infer_mode_from_comment + infer_mode_from_comment, infer_name_from_callsign, infer_latlon_from_callsign, infer_grid_from_callsign # Data class that defines a spot. @@ -130,9 +130,18 @@ class Spot: if self.comment and not self.qrt: self.qrt = "QRT" in self.comment.upper() - # TODO use QRZ/HamQTH provider to get grids, lat Lon, when missing; and DX name - # credentials in config file which is .gitignored; sample provided - # TODO lat/lon from DXCC centre as last resort? + # DX operator details lookup, using QRZ.com. This should be the last resort compared to taking the data from + # the actual spotting 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_call and not self.latitude: + latlon = infer_latlon_from_callsign(self.dx_call) + if latlon: + self.latitude = latlon[0] + self.longitude = latlon[1] + if self.dx_call and not self.grid: + self.grid = infer_grid_from_callsign(self.dx_call) # JSON serialise def to_json(self): diff --git a/main.py b/main.py index e53bd1e..bf48609 100644 --- a/main.py +++ b/main.py @@ -33,11 +33,6 @@ if __name__ == '__main__': # Set up logging root = logging.getLogger() root.setLevel(logging.INFO) - handler = logging.StreamHandler(sys.stdout) - handler.setLevel(logging.INFO) - formatter = logging.Formatter("%(message)s") - handler.setFormatter(formatter) - root.addHandler(handler) logging.info("Starting...") # Shut down gracefully on SIGINT @@ -45,13 +40,13 @@ if __name__ == '__main__': # Create providers providers = [ - POTA(), - SOTA(), - WWFF(), - WWBOTA(), - GMA(), - HEMA(), - ParksNPeaks(), + # POTA(), + # SOTA(), + # WWFF(), + # WWBOTA(), + # GMA(), + # HEMA(), + # ParksNPeaks(), DXCluster("hrd.wa9pie.net", 8000), # DXCluster("dxc.w3lpl.net", 22) ]