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())