diff --git a/not1mm/__main__.py b/not1mm/__main__.py index fa1a3576..4d2f50ac 100644 --- a/not1mm/__main__.py +++ b/not1mm/__main__.py @@ -77,6 +77,7 @@ from not1mm.radio import Radio from not1mm.voice_keying import Voice from not1mm.lookupservice import LookupService +from not1mm.rtc_service import RTCService poll_time = datetime.datetime.now() @@ -102,6 +103,11 @@ class MainWindow(QtWidgets.QMainWindow): "multicast_group": "239.1.1.1", "multicast_port": 2239, "interface_ip": "0.0.0.0", + "send_rtc_scores": False, + "rtc_url": "", + "rtc_user": "", + "rtc_pass": "", + "rtc_interval": 2, "send_n1mm_packets": False, "n1mm_station_name": "20M CW Tent", "n1mm_operator": "Bernie", @@ -156,7 +162,6 @@ class MainWindow(QtWidgets.QMainWindow): opon_dialog = None dbname = fsutils.USER_DATA_PATH, "/ham.db" radio_state = {} - rig_control = None worked_list = {} cw_entry_visible = False last_focus = None @@ -170,6 +175,7 @@ class MainWindow(QtWidgets.QMainWindow): radio_thread = QThread() voice_thread = QThread() fldigi_thread = QThread() + rtc_thread = QThread() fldigi_watcher = None rig_control = None @@ -179,6 +185,7 @@ class MainWindow(QtWidgets.QMainWindow): vfo_window = None lookup_service = None fldigi_util = None + rtc_service = None current_widget = None @@ -712,7 +719,7 @@ def __init__(self, splash): ) def load_call_history(self) -> None: - """""" + """Display filepicker and load chosen call history file.""" filename = self.filepicker("other") if filename: self.database.create_callhistory_table() @@ -755,13 +762,13 @@ def load_call_history(self) -> None: self.show_message_box(f"{err}") def on_focus_changed(self, new): - """""" + """Called when text entry focus has changed.""" if self.use_esm: if hasattr(self.contest, "process_esm"): self.contest.process_esm(self, new_focused_widget=new) def make_button_green(self, the_button: QtWidgets.QPushButton) -> None: - """Turn the_button green.""" + """Takes supplied QPushButton object and turns it green.""" if the_button is not None: pal = QPalette() pal.isCopyOf(self.current_palette) @@ -2457,7 +2464,10 @@ def save_contact(self) -> None: self.worked_list = self.database.get_calls_and_bands() self.send_worked_list() self.clearinputs() - + if self.pref.get("send_rtc_scores", False): + if hasattr(self.contest, "online_score_xml"): + if self.rtc_service is not None: + self.rtc_service.xml = self.contest.online_score_xml(self) cmd = {} cmd["cmd"] = "UPDATELOG" if self.log_window: @@ -2898,6 +2908,25 @@ def readpreferences(self) -> None: self.setDarkMode(False) self.actionDark_Mode_2.setChecked(False) + try: + if self.rtc_thread.isRunning(): + self.rtc_service.time_to_quit = True + self.rtc_thread.quit() + self.rtc_thread.wait(1000) + + except (RuntimeError, AttributeError): + ... + + self.rtc_service = None + + if self.pref.get("send_rtc_scores", False): + self.rtc_service = RTCService() + self.rtc_service.moveToThread(self.rtc_thread) + self.rtc_thread.started.connect(self.rtc_service.run) + self.rtc_thread.finished.connect(self.rtc_service.deleteLater) + # self.rtc_service.poll_callback.connect(self.rtc_result) + self.rtc_thread.start() + try: if self.radio_thread.isRunning(): self.rig_control.time_to_quit = True @@ -3046,6 +3075,12 @@ def readpreferences(self) -> None: self.esm_dict["MYCALL"] = fkey_dict.get(self.pref.get("esm_mycall", "DISABLED")) self.esm_dict["QSOB4"] = fkey_dict.get(self.pref.get("esm_qsob4", "DISABLED")) + self.send_rtc_scores = self.pref.get("send_rtc_scores", False) + self.rtc_url = self.pref.get("rtc_url", "") + self.rtc_user = self.pref.get("rtc_user", "") + self.rtc_pass = self.pref.get("rtc_pass", "") + self.rtc_interval = self.pref.get("rtc_interval", 2) + def dark_mode_state_changed(self) -> None: """Called when the Dark Mode menu state is changed.""" self.pref["darkmode"] = self.actionDark_Mode_2.isChecked() diff --git a/not1mm/data/configuration.ui b/not1mm/data/configuration.ui index 44852bb2..f6684e81 100644 --- a/not1mm/data/configuration.ui +++ b/not1mm/data/configuration.ui @@ -2173,6 +2173,76 @@ + + + + Qt::Horizontal + + + + + + + Use RTC score reporting + + + + + + + + https://hamscore.com/postxml/ + + + + + https://contestonlinescore.com/post/ + + + + + http://contest.run + + + + + + + + username + + + + + + + QLineEdit::EchoMode::Password + + + password + + + + + + + Score posting interval (minutes) + + + Qt::AlignCenter + + + + + + + 2 + + + Qt::AlignCenter + + + diff --git a/not1mm/lib/plugin_common.py b/not1mm/lib/plugin_common.py index f72a4c16..3d727730 100644 --- a/not1mm/lib/plugin_common.py +++ b/not1mm/lib/plugin_common.py @@ -4,6 +4,58 @@ from decimal import Decimal from pathlib import Path from not1mm.lib.ham_utility import get_adif_band +from not1mm.lib.version import __version__ + + +def online_score_xml(self): + """generate online xml""" + + mults = self.contest.get_mults(self) + the_mults = "" + for thing in mults: + the_mults += ( + f'{mults.get(thing,0)}' + ) + + the_points = self.contest.just_points(self) + + the_date_time = datetime.datetime.now(datetime.timezone.utc).isoformat(" ")[:19] + assisted = self.contest_settings.get("AssistedCategory", "") + bands = self.contest_settings.get("BandCategory", "") + modes = self.contest_settings.get("ModeCategory", "") + xmiter = self.contest_settings.get("TransmitterCategory", "") + ops = self.contest_settings.get("OperatorCategory", "") + overlay = self.contest_settings.get("OverlayCategory", "") + power = self.contest_settings.get("PowerCategory", "") + + the_xml = ( + '' + "" + f"{self.contest.cabrillo_name}" + f'{self.station.get("Call", "")}' + # NR9Q + f'' + f"{self.station.get('Club', '').upper()}" + "Not1MM" + f"{__version__}" + "" + # K + f"{self.station.get('CQZone','')}" + f"{self.station.get('IARUZone','')}" + f"{self.station.get('ARRLSection', '')}" + f"{self.station.get('State','')}" + f"{self.station.get('GridSquare','')}" + "" + "" + f'{self.contest.show_qso(self)}' + f"{the_mults}" + f'{the_points}' + "" + f"{self.contest.calc_score(self)}" + f"{the_date_time}" + "" + ) + return the_xml def get_points(self): diff --git a/not1mm/lib/settings.py b/not1mm/lib/settings.py index 4a5f5b3d..38dc93b3 100644 --- a/not1mm/lib/settings.py +++ b/not1mm/lib/settings.py @@ -41,6 +41,19 @@ def __init__(self, app_data_path, pref, parent=None): def setup(self): """setup dialog""" + self.send_rtc_scores.setChecked( + bool(self.preference.get("send_rtc_scores", False)) + ) + + value = self.preference.get("rtc_url", "") + index = self.rtc_url.findText(value) + if index != -1: + self.rtc_url.setCurrentIndex(index) + + self.rtc_user.setText(str(self.preference.get("rtc_user", ""))) + self.rtc_pass.setText(str(self.preference.get("rtc_pass", ""))) + self.rtc_interval.setText(str(self.preference.get("rtc_interval", "2"))) + self.use_call_history.setChecked( bool(self.preference.get("use_call_history", False)) ) @@ -195,6 +208,15 @@ def save_changes(self): """ Write preferences to json file. """ + self.preference["send_rtc_scores"] = self.send_rtc_scores.isChecked() + self.preference["rtc_url"] = self.rtc_url.currentText() + self.preference["rtc_user"] = self.rtc_user.text() + self.preference["rtc_pass"] = self.rtc_pass.text() + try: + self.preference["rtc_interval"] = int(self.rtc_interval.text()) + except ValueError: + self.preference["rtc_interval"] = 2 + self.preference["use_call_history"] = self.use_call_history.isChecked() self.preference["use_esm"] = self.use_esm.isChecked() self.preference["esm_cq"] = self.esm_cq.currentText() diff --git a/not1mm/lookupservice.py b/not1mm/lookupservice.py index e030bb5e..91733fcc 100755 --- a/not1mm/lookupservice.py +++ b/not1mm/lookupservice.py @@ -3,8 +3,8 @@ not1mm Contest logger Email: michael.bridak@gmail.com GPL V3 -Class: BandMapWindow -Purpose: Onscreen widget to show realtime spots from an AR cluster. +Class: LookupService +Purpose: Lookup callsigns with online services. """ # pylint: disable=unused-import, c-extension-no-member, no-member, invalid-name, too-many-lines diff --git a/not1mm/plugins/cq_ww_cw.py b/not1mm/plugins/cq_ww_cw.py index 7fdbf187..4d49cbc3 100644 --- a/not1mm/plugins/cq_ww_cw.py +++ b/not1mm/plugins/cq_ww_cw.py @@ -43,7 +43,7 @@ from PyQt6 import QtWidgets -from not1mm.lib.plugin_common import gen_adif, get_points +from not1mm.lib.plugin_common import gen_adif, get_points, online_score_xml from not1mm.lib.version import __version__ from not1mm.lib.ham_utility import get_logged_band @@ -177,6 +177,19 @@ def points(self): return 0 +def get_mults(self): + """""" + mults = {} + mults["zone"] = self.database.fetch_zn_band_count().get("zb_count", 0) + mults["country"] = self.database.fetch_country_band_count().get("cb_count", 0) + return mults + + +def just_points(self): + """""" + return self.database.fetch_points().get("Points", "0") + + def show_mults(self): """Return display string for mults""" result1 = self.database.fetch_zn_band_count() diff --git a/not1mm/plugins/cwt.py b/not1mm/plugins/cwt.py index 5d70848b..c47e50d3 100644 --- a/not1mm/plugins/cwt.py +++ b/not1mm/plugins/cwt.py @@ -33,7 +33,7 @@ from PyQt6 import QtWidgets -from not1mm.lib.plugin_common import gen_adif, get_points +from not1mm.lib.plugin_common import gen_adif, get_points, online_score_xml from not1mm.lib.version import __version__ logger = logging.getLogger(__name__) @@ -519,3 +519,23 @@ def check_call_history(self): self.other_1.setText(f"{result.get('Name', '')}") if self.other_2.text() == "": self.other_2.setText(f"{result.get('Exch1', '')}") + + +# --------RTC Stuff----------- +def get_mults(self): + """""" + + mults = {} + mults["state"] = show_mults(self) + return mults + + +def just_points(self): + """""" + result = self.database.fetch_points() + if result is not None: + score = result.get("Points", "0") + if score is None: + score = "0" + return int(score) + return 0 diff --git a/not1mm/plugins/icwc_mst.py b/not1mm/plugins/icwc_mst.py index 8d17c438..c31ef1b4 100644 --- a/not1mm/plugins/icwc_mst.py +++ b/not1mm/plugins/icwc_mst.py @@ -34,7 +34,7 @@ from PyQt6 import QtWidgets -from not1mm.lib.plugin_common import gen_adif, get_points +from not1mm.lib.plugin_common import gen_adif, get_points, online_score_xml from not1mm.lib.version import __version__ logger = logging.getLogger(__name__) @@ -493,3 +493,23 @@ def check_call_history(self): self.history_info.setText(f"{result.get('UserText','')}") if self.other_2.text() == "": self.other_2.setText(f"{result.get('Name', '')}") + + +# --------RTC Stuff----------- +def get_mults(self): + """""" + + mults = {} + mults["state"] = show_mults(self) + return mults + + +def just_points(self): + """""" + result = self.database.fetch_points() + if result is not None: + score = result.get("Points", "0") + if score is None: + score = "0" + return int(score) + return 0 diff --git a/not1mm/rtc_service.py b/not1mm/rtc_service.py new file mode 100644 index 00000000..d4208ae8 --- /dev/null +++ b/not1mm/rtc_service.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +not1mm Contest logger +Email: michael.bridak@gmail.com +GPL V3 +Class: RTCService +Purpose: Service to post 'real time' scores. +""" + +# pylint: disable=unused-import, c-extension-no-member, no-member, invalid-name, too-many-lines +# pylint: disable=logging-fstring-interpolation, line-too-long, no-name-in-module + +import datetime +import logging +import os +from json import loads + +import requests +from requests.auth import HTTPBasicAuth + +from PyQt6.QtCore import QObject, pyqtSignal, QThread, QEventLoop + +import not1mm.fsutils as fsutils + +logger = logging.getLogger(__name__) + + +class RTCService(QObject): + """The RTC Service class.""" + + poll_callback = pyqtSignal(dict) + delta = 2 # two minutes + poll_time = datetime.datetime.now() + datetime.timedelta(minutes=delta) + time_to_quit = False + xml = "" + + def __init__(self): + super().__init__() + self.pref = self.get_settings() + self.delta = self.pref.get("rtc_interval", 2) + + def run(self) -> None: + """Send score xml object to rtc scoring site.""" + while not self.time_to_quit: + # if self.pref.get("send_rtc_scores", False) is True: + if datetime.datetime.now() > self.poll_time: + self.poll_time = datetime.datetime.now() + datetime.timedelta( + minutes=self.delta + ) + if len(self.xml): + headers = {"Content-Type": "text/xml"} + try: + result = requests.post( + self.pref.get("rtc_url", ""), + data=self.xml, + headers=headers, + auth=HTTPBasicAuth( + self.pref.get("rtc_user", ""), + self.pref.get("rtc_pass", ""), + ), + timeout=30, + ) + print(f"{self.xml=}\n{result=}\n{result.text}") + except requests.exceptions.Timeout: + print("RTC post timeout.") + except requests.exceptions.RequestException as e: + print(f"An RTC post error occurred: {e}") + else: + print("No XML data") + try: + self.poll_callback.emit({"success": True}) + except QEventLoop: + ... + QThread.msleep(1) + + def get_settings(self) -> dict: + """Get the settings.""" + if os.path.exists(fsutils.CONFIG_FILE): + with open(fsutils.CONFIG_FILE, "rt", encoding="utf-8") as file_descriptor: + return loads(file_descriptor.read())