Files
spothole/core/utils.py

242 lines
9.3 KiB
Python

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"]):
print(call + " " + str(entry))
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__