I have some issues when toggle button is set to OFF it goes back to ON as json read still gets not refreshed value and kicks in slot_method. But I need this if statement to read initial value when launching the app and this mode allready can be selected from hardware controller. How it can be reworked that on toggle button click 1 json read thread could be excluded ?
py_toggle.py
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
class PyToggle(QCheckBox):
def __init__(
self,
width=60,
bg_color="#777",
circle_color="#DDD",
active_color="#3A3A66",
animation_curve=QEasingCurve.OutBounce
):
QCheckBox.__init__(self)
# SET DEFAULT PARAMETERS
self.setFixedSize(width, 28)
self.setCursor(Qt.PointingHandCursor)
# COLORS
self._bg_color = bg_color
self._circle_color = circle_color
self._active_color = active_color
# CREATE ANIMATION
self._circle_position = 3
self.animation = QPropertyAnimation(self, b"circle_position", self)
self.animation.setEasingCurve(animation_curve)
self.animation.setDuration(400) # Time in milliseconds
# CONNECT STATE CHANGED
# self.stateChanged.connect(self.debug)
self.stateChanged.connect(self.start_transition)
# self.stateChanged.connect()
# CREATE NEW SET AND GET PROPERTIE
@Property(float) # Decorator Getter
def circle_position(self):
return self._circle_position
@circle_position.setter
def circle_position(self, pos):
self._circle_position = pos
self.update()
# def state(self):
# print(f"Status: {self.isChecked()}")
def start_transition(self, value):
self.animation.stop() # Stop animation if running
if value:
self.animation.setEndValue(self.width() - 26)
else:
self.animation.setEndValue(3)
# START ANIMATION
self.animation.start()
# SET NEW HIT AREA
def hitButton(self, pos: QPoint):
return self.contentsRect().contains(pos)
# DRAW NEW ITEMS
def paintEvent(self, e):
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
# SET AS NO PEN
p.setPen(Qt.NoPen)
# DRAW RECTANGLE
rect = QRect(0, 0, self.width(), self.height())
if not self.isChecked():
# DRAW BG
p.setBrush(QColor(self._bg_color))
p.drawRoundedRect(0, 0, rect.width(), self.height(), self.height() / 2, self.height() / 2)
# DRAW CIRCLE
p.setBrush(QColor(self._circle_color))
p.drawEllipse(self._circle_position, 3, 22, 22)
else:
# DRAW BG
p.setBrush(QColor(self._active_color))
p.drawRoundedRect(0, 0, rect.width(), self.height(), self.height() / 2, self.height() / 2)
# DRAW CIRCLE
p.setBrush(QColor(self._circle_color))
p.drawEllipse(self._circle_position, 3, 22, 22)
# END DRAW
p.end()
test.py
import sys
import requests
from PySide2.QtCore import (QTimer, QThread, Signal)
from PySide2.QtWidgets import *
from py_toggle import PyToggle
class WorkerThread(QThread):
measurements_signals = Signal(str, name = 'm_signals') # declare the signal
def __init__(self, parent=None):
QThread.__init__(self)
self.timer = QTimer()
self.timer.timeout.connect(lambda: WorkerThread.run(self))
self.timer.setInterval(6000) # 6000ms = 6s
self.timer.start()
def run(self):
url = "http://192.168.8.150/json"
try:
res = requests.get(url)
msg = res.json()
print(msg)
try:
if res.status_code == 200:
quiet = msg["heatpump"][18]["Value"]
self.measurements_signals.emit(quiet)
else:
print("Not Working")
except requests.exceptions.InvalidURL or requests.exceptions.ConnectionError as err:
print(err)
except requests.exceptions.InvalidURL or requests.exceptions.ConnectionError as err:
print(err)
def stop(self):
self.terminate()
print("stop")
class Tester(QWidget):
def __init__(self, parent = None):
super(Tester, self).__init__(parent)
# ==> TOGGLE BUTTON1
self.setWindowTitle("Test")
self.resize(200, 150)
layout = QGridLayout()
self.toggle = PyToggle()
layout.addWidget(self.toggle)
self.toggle.stateChanged.connect(self.postCommand)
self.setLayout(layout)
# ==> Worker Thread start
self.wt = WorkerThread() # This is the thread object
self.wt.start()
# Connect the signal from the thread to the slot_method
self.wt.measurements_signals.connect(self.slot_method) ### 3) connect to the slot
app.aboutToQuit.connect(self.wt.stop) # to stop the thread when closing the GUI
def slot_method(self, quiet):
if quiet == "1":
self.toggle.setChecked(True)
def postCommand(self):
if self.toggle.isChecked():
setting = "SetQuietMode=1"
else:
setting = "SetQuietMode=0"
url = f"http://192.168.8.150/command?{setting}"
r = requests.request('GET', url)
if __name__ == "__main__":
app = QApplication(sys.argv)
form = Tester()
form.show()
sys.exit(app.exec_())
CodePudding user response:
There are two problems in your implementation.
The first one is that the QThread actually runs the function in the separate thread just the first time (when start()
is called), while the QTimer
will always execute it in the main thread, where it was created. run()
should never be explicitly called.
Then, and this is the main point of your issue, the signal should always be emitted only when the value changes, not every time it is requested.
Finally, since requests are potentially blocking, the command sent from the UI should also be threaded. We already have a thread running, so we could use that along with a python Queue
.
There are also other issues:
- as the documentation explicitly says,
terminate()
is discouraged, as using it could cause problems and stability issues; using a simple flag is way more simple and safer; in any case, with your original code it wouldn't do nothing anyway, unless called as soon as the thread is started: as stated above, each further execution was done in the wrong thread, soterminate()
will do nothing useful; - the second
try
/except
block is pointless, as there is no possibility for that type of exception in there; the only exception that would make sense at that point is theKeyError
for the dictionary lookup; - the
stateChanged
signal is for tri-state checkboxes, for standard dual state the proper signal istoggled
;
A proper implementation must keep the thread running in a while
loop, and eventually quit when necessary. Using the queue timeout
argument we also get the 6 second interval, so there is no need for timers.
from queue import Queue, Empty
class WorkerThread(QThread):
measurements_signals = Signal(str, name = 'm_signals')
def __init__(self, parent=None):
QThread.__init__(self, parent)
self.queue = Queue()
def run(self):
self.keepRunning = True
url = "http://192.168.8.150/"
quiet = None
while self.keepRunning:
try:
res = requests.get(url 'json')
msg = res.json()
print(msg)
if res.status_code == 200:
new = msg["heatpump"][18]["Value"]
if new != quiet:
quiet = new
self.measurements_signals.emit(quiet)
else:
print("Not Working")
except requests.exceptions.InvalidURL or requests.exceptions.ConnectionError as err:
print(err)
try:
q = self.queue.get(timeout=6)
if q == -1:
break
cmd, value = q
if cmd == 'SetQuietMode':
quiet = value
requests.request('GET', f'{url}command?{cmd}={value}')
except Empty:
pass
def stop(self):
print("stop")
self.keepRunning = False
self.queue.put(-1)
self.wait()
def setQuiet(self, state):
self.queue.put(('SetQuietMode', int(state)))
class Tester(QWidget):
def __init__(self, parent=None):
# ...
self.wt = WorkerThread()
self.wt.measurements_signals.connect(self.slot_method)
app.aboutToQuit.connect(self.wt.stop)
self.toggle.toggled.connect(self.wt.setQuiet)
# it's usually better to start the thread *after* connecting signals
self.wt.start()
def slot_method(self, quiet):
if quiet == "1":
# temporarily disconnect to avoid calling setQuiet unnecessarily
self.toggle.toggled.disconnect(self.wt.setQuiet)
self.toggle.setChecked(True)
self.toggle.toggled.connect(self.wt.setQuiet)
Note that QtNetwork module provides the QNetworkAccessManager, which already works asynchronously. Using that, you can avoid creating a separate thread class and you will be able to extend the program's functionality much more easily.