Home > Software design >  Qthread with reading json values and sending json with toggle button issues
Qthread with reading json values and sending json with toggle button issues

Time:02-27

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, so terminate() 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 the KeyError for the dictionary lookup;
  • the stateChanged signal is for tri-state checkboxes, for standard dual state the proper signal is toggled;

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.

  • Related