From 6b2df8417ba5910fffd64c1119784d363b5402b4 Mon Sep 17 00:00:00 2001 From: Michael Clemens Date: Thu, 20 May 2021 18:05:33 +0200 Subject: [PATCH] first commit --- README.md | 40 +++++++ config.ini_dist | 6 + qrzlogger.py | 285 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+) create mode 100644 README.md create mode 100644 config.ini_dist create mode 100644 qrzlogger.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd2278c --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +qrzlogger +========= + +This script is a QRZ.com command line QSO logger. +It does the following: + 1) asks the user for a call sign + 2) displays available call sign info pulled from QRZ.com + 3) displays all previous QSOs with this call (pulled from QRZ.com logbook) + 4) alles the user to enter QSO specific data (date, time, report, band etc.) + 5) uploads the QSO to QRZ.com's logbook + 6) lists the last 5 logged QSOs ((pulled from QRZ.com logbook) + 7) starts again from 1) + + +MIT License + +Copyright (c) 2021 Michael Clemens, DL6MHC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +WARNING: This software is beta and is really not working properly yet! +I'll remove this warning when it's done + diff --git a/config.ini_dist b/config.ini_dist new file mode 100644 index 0000000..dea1293 --- /dev/null +++ b/config.ini_dist @@ -0,0 +1,6 @@ +[qrzlogger] +api_url = https://logbook.qrz.com/api +api_key = 1234-ABCD-1234-A1B2 +qrz_user = N0CALL +qrz_pass = q1w2e3r4t5z6u7i8o9 +xml_fields = ("call", "band", "mode", "qso_date", "time_on", "rst_sent", "rst_rcvd", "comment") diff --git a/qrzlogger.py b/qrzlogger.py new file mode 100644 index 0000000..0183812 --- /dev/null +++ b/qrzlogger.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 + +# qrzlogger +# ========= +# +# This script is a QRZ.com command line QSO logger. +# It does the following: +# 1) asks the user for a call sign +# 2) displays available call sign info pulled from QRZ.com +# 3) displays all previous QSOs with this call (pulled from QRZ.com logbook) +# 4) alles the user to enter QSO specific data (date, time, report, band etc.) +# 5) uploads the QSO to QRZ.com's logbook +# 6) lists the last 5 logged QSOs ((pulled from QRZ.com logbook) +# 7) starts again from 1) +# +# +# MIT License +# +# Copyright (c) 2021 Michael Clemens, DL6MHC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +# WARNING: This software is beta and is really not working properly yet! +# I'll remove this warning when it's done + + +import requests +import urllib +import re +import datetime +import os +import xmltodict +from prettytable import PrettyTable +from requests.structures import CaseInsensitiveDict +from datetime import date +from datetime import timezone +import configparser + +# read in the config +config = configparser.ConfigParser() +config.read('config.ini') + +headers = CaseInsensitiveDict() +headers["Content-Type"] = "application/x-www-form-urlencoded" + +session = None +session_key = None + + +# Generate a session for QRZ.com's xml service with +# the help of the QRZ.com username and password +def get_session(): + global session + global session_key + xml_auth_url = '''https://xmldata.QRZ.com/xml/current/?username={0}&password={1}'''.format( + config['qrzlogger']['qrz_user'],config['qrzlogger']['qrz_pass']) + session = requests.Session() + session.verify = bool(os.getenv('SSL_VERIFY', True)) + r = session.get(xml_auth_url) + if r.status_code == 200: + raw_session = xmltodict.parse(r.content) + session_key = raw_session.get('QRZDatabase').get('Session').get('Key') + if session_key: + return True + return False + + +# Query QRZ.com's xml api to gather information +# about a specific call sign +def getCallData(call): + global session + global session_key + + xml_url = """https://xmldata.QRZ.com/xml/current/?s={0}&callsign={1}""" .format(session_key, call) + r = session.get(xml_url) + if r.status_code != 200: + print("nope") + raw = xmltodict.parse(r.content).get('QRZDatabase') + if not raw: + print("nope") + if raw['Session'].get('Error'): + errormsg = raw['Session'].get('Error') + else: + calldata = raw.get('Callsign') + if calldata: + return calldata + return "nope" + + +# Query QRZ.com's logbook for all previous QSOs +# with a specific call sign +def getQSOsForCallsign(callsign): + option = "TYPE:ADIF,CALL:"+callsign + fetch = { 'KEY' : config['qrzlogger']['api_key'], 'ACTION' : 'FETCH', 'OPTION' : option} + data = urllib.parse.urlencode(fetch) + + resp = requests.post(config['qrzlogger']['api_url'], headers=headers, data=data) + + str_resp = resp.content.decode("utf-8") + response = urllib.parse.unquote(str_resp) + + resp_list = response.splitlines() + result = [{}] + for i in resp_list: + if not i: + result.append({}) + else: + if any(s+":" in i for s in config['qrzlogger']['xml_fields']): + i = re.sub('<','',i, flags=re.DOTALL) + i = re.sub(':.*>',":",i, flags=re.DOTALL) + v = re.sub('^.*:',"",i, flags=re.DOTALL) + k = re.sub(':.*$',"",i, flags=re.DOTALL) + result[-1][k] = v + + return result + + +# Generate a pretty ascii table containing all +# previous QSOs with a specific call sign +def getQSOTable(result): + t = PrettyTable(['Date', 'Time', 'Band', 'Mode', 'RST-S', 'RST-R', 'Comment']) + for d in result: + if "qso_date" in d: + date = datetime.datetime.strptime(d["qso_date"], '%Y%m%d').strftime('%Y/%m/%d') + time = datetime.datetime.strptime(d["time_on"], '%H%M').strftime('%H:%M') + comment = "" + try: + comment = d["comment"] + except: + comment = "" + t.add_row([date, time, d["band"], d["mode"], d["rst_sent"], d["rst_rcvd"], comment]) + t.align = "r" + return t + +# Print a pretty ascii table containing all interesting +# data found for a specific call sign +def getXMLQueryTable(result): + t = PrettyTable(['key', 'value']) + if "fname" in result: + t.add_row(["First Name", result["fname"]]) + if "name" in result: + t.add_row(["Last Name", result["name"]]) + if "addr1" in result: + t.add_row(["Street", result["addr1"]]) + if "addr2" in result: + t.add_row(["City", result["addr2"]]) + if "state" in result: + t.add_row(["State", result["state"]]) + if "country" in result: + t.add_row(["Country", result["country"]]) + if "grid" in result: + t.add_row(["Locator", result["grid"]]) + if "email" in result: + t.add_row(["Email", result["email"]]) + if "qslmgr" in result: + t.add_row(["QSL via:", result["qslmgr"]]) + t.align = "l" + t.header = False + return t + + +# Print a pretty ascii table containing all +# previously entered user data +def getQSODetailTable(qso): + t = PrettyTable(['key', 'value']) + for q in qso: + t.add_row([qso[q][0], qso[q][1]]) + t.align = "l" + t.header = False + return t + + +# Queries QSO specific data from the user via +# the command line +def queryQSOData(qso): + dt = datetime.datetime.now(timezone.utc) + dt_now = dt.replace(tzinfo=timezone.utc) + + qso_date = dt_now.strftime("%Y%m%d") + qso_time = dt_now.strftime("%H%M") + band = "40m" + mode = "SSB" + r_rcvd = "59" + r_sent = "59" + power = "100" + comment = "" + + if qso is None: + questions = { + "qso_date" : ["QSO Date: ",qso_date], + "qso_time": ["QSO Time: ", qso_time], + "band": ["Band: ", band], + "mode": ["Mode: ", mode], + "r_rcvd": ["RPT Received: ", r_rcvd], + "r_sent": ["RPT Sent: ", r_sent], + "power": ["Power (in W): ", power], + "comment": ["Comment: ", comment] + } + else: + questions = qso + + for q in questions: + inp = input(questions[q][0]+" ["+questions[q][1]+"]: " ) + if inp != "": + questions[q][1] = inp + + return questions + + +# Sends the previously collected QSO information as a new +# QRZ.com logbook entry via the API +def sendQSO(qso): + insert = { 'KEY' : config['qrzlogger']['api_key'], 'ACTION' : 'INSERT', 'ADIF' : + '' + qso["band"][1] + + '' + qso["mode"][1] + + '' + call + + '' + qso["qso_date"][1] + + 'DL6MHC' + + '' + qso["qso_time"][1] + + ''} + + data = urllib.parse.urlencode(insert) + resp = requests.post(config['qrzlogger']['api_url'], headers=headers, data=data) + + str_resp = resp.content.decode("utf-8") + response = urllib.parse.unquote(str_resp) + print(response) + + return result + + +# Main routine +if __name__ == '__main__': + + get_session() + call = input("Enter Callsign: ") + + print('\nQRZ.com results for {0}:\n'.format(call)) + + result = getCallData(call) + tab = getXMLQueryTable(result) + print(tab) + + print('\n\nPrevious QSOs with {0}:\n'.format(call)) + + result = getQSOsForCallsign(call) + tab = getQSOTable(result) + print(tab) + + print('\nEnter new QSO details below ("c" to cancel, "b" to go one entry back):\n') + + qso_ok = False + qso = None + + while not qso_ok: + # query QSO details from thbe user + qso = queryQSOData(qso) + # generate a pretty table + tab = getQSODetailTable(qso) + print(tab) + # ask user if everything is ok. If not, start over. + inp = input("Is this correct? [y/n]: " ) + if inp == "y": + res = sendQSO(qso) + print(res) + + qso_ok = True +