import logging from diskcache import Cache from pyhamtools import LookupLib, Callinfo from pyhamtools.frequency import freq_to_band from pyhamtools.locator import latlong_to_locator from core.config import config from core.constants import BANDS, UNKNOWN_BAND, CW_MODES, PHONE_MODES, DATA_MODES, ALL_MODES, QRZCQ_CALLSIGN_LOOKUP_DATA # 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 = Cache('.qrz_callsign_lookup_cache') # Infer a mode from the comment def infer_mode_from_comment(comment): for mode in ALL_MODES: if mode in comment.upper(): return mode return None # Infer a "mode family" from a mode. def infer_mode_type_from_mode(mode): if mode.upper() in CW_MODES: return "CW" elif mode.upper() in PHONE_MODES: return "PHONE" elif mode.upper() in DATA_MODES: return "DATA" else: if mode.upper() != "OTHER": logging.warn("Found an unrecognised mode: " + mode + ". Developer should categorise this.") return None # Infer a band from a frequency in Hz def infer_band_from_freq(freq): for b in BANDS: if b.start_freq <= freq <= b.end_freq: return b return UNKNOWN_BAND # Infer a country name from a callsign def infer_country_from_callsign(call): try: # Use the full callsign, falling back to the base callsign, assuming this will be the longest of any /-separated # sections. country = CALL_INFO_BASIC.get_country_name(call) if not country: base_call = max(call.split("/"), key=len) country = CALL_INFO_BASIC.get_country_name(base_call) except KeyError as e: country = None # Couldn't get anything from basic call info database, try QRZ.com if not country: qrz_data = get_qrz_data_for_callsign(call) if qrz_data and "country" in qrz_data: country = qrz_data["country"] # Couldn't get anything from QRZ.com database, try QRZCQ data if not country: qrzcq_data = get_qrzcq_data_for_callsign(call) if qrzcq_data and qrzcq_data["country"]: country = qrzcq_data["country"] return country # Infer a DXCC ID from a callsign def infer_dxcc_id_from_callsign(call): try: # Start with the basic country-files.com-based decoder. Use the full callsign, falling back to the base # callsign, assuming this will be the longest of any /-separated sections. Then if that doesn't provide data, # and we have QRZ data, try the same with that. dxcc = CALL_INFO_BASIC.get_adif_id(call) if not dxcc: base_call = max(call.split("/"), key=len) dxcc = CALL_INFO_BASIC.get_adif_id(base_call) except KeyError as e: dxcc = None # Couldn't get anything from basic call info database, try QRZ.com if not dxcc: qrz_data = get_qrz_data_for_callsign(call) if qrz_data and "adif" in qrz_data: dxcc = qrz_data["adif"] # Couldn't get anything from QRZ.com database, try QRZCQ data if not dxcc: qrzcq_data = get_qrzcq_data_for_callsign(call) if qrzcq_data and qrzcq_data["dxcc"]: dxcc = qrzcq_data["dxcc"] return dxcc # Infer a continent shortcode from a callsign def infer_continent_from_callsign(call): try: # Start with the basic country-files.com-based decoder. Use the full callsign, falling back to the base # callsign, assuming this will be the longest of any /-separated sections. continent = CALL_INFO_BASIC.get_continent(call) if not continent: base_call = max(call.split("/"), key=len) continent = CALL_INFO_BASIC.get_continent(base_call) except KeyError as e: continent = None # Couldn't get anything from basic call info database, try QRZCQ data if not continent: qrzcq_data = get_qrzcq_data_for_callsign(call) if qrzcq_data and qrzcq_data["continent"]: continent = qrzcq_data["continent"] return continent # Infer a CQ zone from a callsign def infer_cq_zone_from_callsign(call): try: # Start with the basic country-files.com-based decoder. Use the full callsign, falling back to the base # callsign, assuming this will be the longest of any /-separated sections. Then if that doesn't provide data, # and we have QRZ data, try the same with that. cqz = CALL_INFO_BASIC.get_cqz(call) if not cqz: base_call = max(call.split("/"), key=len) cqz = CALL_INFO_BASIC.get_cqz(base_call) except KeyError as e: cqz = None # Couldn't get anything from basic call info database, try QRZ.com if not cqz: qrz_data = get_qrz_data_for_callsign(call) if qrz_data and "cqz" in qrz_data: cqz = qrz_data["cqz"] # Couldn't get anything from QRZ.com database, try QRZCQ data if not cqz: qrzcq_data = get_qrzcq_data_for_callsign(call) if qrzcq_data and qrzcq_data["cqz"]: cqz = qrzcq_data["cqz"] return cqz # Infer a ITU zone from a callsign def infer_itu_zone_from_callsign(call): try: # Start with the basic country-files.com-based decoder. Use the full callsign, falling back to the base # callsign, assuming this will be the longest of any /-separated sections. Then if that doesn't provide data, # and we have QRZ data, try the same with that. ituz = CALL_INFO_BASIC.get_ituz(call) if not ituz: base_call = max(call.split("/"), key=len) ituz = CALL_INFO_BASIC.get_ituz(base_call) except KeyError as e: ituz = None # Couldn't get anything from basic call info database, try QRZ.com if not ituz: qrz_data = get_qrz_data_for_callsign(call) if qrz_data and "ituz" in qrz_data: ituz = qrz_data["ituz"] # Couldn't get anything from QRZ.com database, try QRZCQ data if not ituz: qrzcq_data = get_qrzcq_data_for_callsign(call) if qrzcq_data and qrzcq_data["ituz"]: ituz = qrzcq_data["ituz"] return ituz # 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 qrz_data = QRZ_CALLSIGN_DATA_CACHE.get(call) if qrz_data: return qrz_data elif QRZ_AVAILABLE: try: data = LOOKUP_LIB_QRZ.lookup_callsign(callsign=call) QRZ_CALLSIGN_DATA_CACHE.add(call, data, expire=604800) # 1 week in seconds return data except KeyError: # QRZ had no info for the call, that's OK return None else: return None # Utility method to get QRZCQ data from our constants table, if we can find it def get_qrzcq_data_for_callsign(call): # Iterate in reverse order - see comments on the data structure itself for entry in reversed(QRZCQ_CALLSIGN_LOOKUP_DATA): if call.startswith(entry["prefix"]): return entry 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_qrz(call): data = get_qrz_data_for_callsign(call) if data and "latitude" in data and "longitude" in data: return [data["latitude"], data["longitude"]] else: return None # Infer a grid locator from a callsign (requires QRZ.com) def infer_grid_from_callsign_qrz(call): data = get_qrz_data_for_callsign(call) if data and "locator" in data: return data["locator"] else: return None # Infer a latitude and longitude from a callsign (using DXCC, probably very inaccurate) def infer_latlon_from_callsign_dxcc(call): try: data = CALL_INFO_BASIC.get_lat_long(call) if data and "latitude" in data and "longitude" in data: return [data["latitude"], data["longitude"]] else: return None except KeyError: return None # Infer a grid locator from a callsign (using DXCC, probably very inaccurate) def infer_grid_from_callsign_dxcc(call): latlon = infer_latlon_from_callsign_dxcc(call) return latlong_to_locator(latlon[0], latlon[1], 8) # Infer a mode from the frequency (in Hz) according to the band plan. Just a guess really. def infer_mode_from_frequency(freq): try: return freq_to_band(freq / 1000.0)["mode"] except KeyError: return None # Convert objects to serialisable things. Used by JSON serialiser as a default when it encounters unserializable things. # Just converts objects to dict. Try to avoid doing anything clever here when serialising spots, because we also need # to receive spots without complex handling. def serialize_everything(obj): return obj.__dict__