MqttMonitor/MqttMonitorWidget.py

branch
eric7
changeset 102
70b8858199f5
parent 101
0eae5f616154
child 103
5fe4f179975f
--- a/MqttMonitor/MqttMonitorWidget.py	Tue Jul 20 18:10:55 2021 +0200
+++ b/MqttMonitor/MqttMonitorWidget.py	Wed Jul 21 20:10:36 2021 +0200
@@ -12,9 +12,9 @@
 import copy
 import contextlib
 
-from PyQt6.QtCore import pyqtSlot, Qt, QTimer, QFileInfo
+from PyQt6.QtCore import pyqtSlot, Qt, QTimer, QFileInfo, QPoint
 from PyQt6.QtGui import QFont, QTextCursor, QBrush, QColor
-from PyQt6.QtWidgets import QWidget, QDialog
+from PyQt6.QtWidgets import QWidget, QDialog, QMenu
 
 from EricWidgets import EricMessageBox, EricFileDialog
 from EricWidgets.EricPathPicker import EricPathPickerModes
@@ -75,10 +75,17 @@
         self.__messagesQosFormat = self.messagesEdit.currentCharFormat()
         self.__messagesQosFormat.setFontItalic(True)
         
+        self.__propertiesFormat = self.propertiesEdit.currentCharFormat()
+        self.__propertiesTopicFormat = self.propertiesEdit.currentCharFormat()
+        self.__propertiesTopicFormat.setFontWeight(QFont.Weight.Bold)
+        self.__propertiesNameFormat = self.propertiesEdit.currentCharFormat()
+        self.__propertiesNameFormat.setFontItalic(True)
+        
         self.messagesSearchWidget.attachTextEdit(self.messagesEdit)
         self.messagesSearchWidget.setWidthForHeight(False)
         
-        self.__isAlternate = False
+        self.__isMessageAlternate = False
+        self.__isPropertiesAlternate = False
         
         for logLevel in (MqttClient.LogDisabled,
                          MqttClient.LogDebug,
@@ -131,7 +138,19 @@
         
         self.subscribeButton.setIcon(UI.PixmapCache.getIcon("plus"))
         self.subscribeButton.setEnabled(False)
+        self.subscribePropertiesButton.setIcon(
+            UI.PixmapCache.getIcon("listSelection"))
+        self.subscribePropertiesButton.setEnabled(False)
+        self.subscribePropertiesButton.setVisible(False)
+        
         self.unsubscribeButton.setIcon(UI.PixmapCache.getIcon("minus"))
+        self.unsubscribeButton.setEnabled(False)
+        self.unsubscribePropertiesButton.setIcon(
+            UI.PixmapCache.getIcon("listSelection"))
+        self.unsubscribePropertiesButton.setEnabled(False)
+        self.unsubscribePropertiesButton.setVisible(False)
+        
+        self.__initPropertiesEditMenu()
         
         self.__subscribedTopics = []
         self.__topicQueue = {}
@@ -187,10 +206,6 @@
         self.__statusLoadValues = collections.defaultdict(
             self.__loadDefaultDictFactory)
     
-    #######################################################################
-    ## Slots handling MQTT related signals
-    #######################################################################
-    
     # TODO: make MQTT default protocol version a configuration option
     # (config page)
     def __createClient(self, clientId="", cleanSession=None,
@@ -229,13 +244,35 @@
         
         return client
     
+    def __initPropertiesEditMenu(self):
+        """
+        Private method to create the properties output context menu.
+        """
+        self.__propertiesEditMenu = QMenu(self)
+        self.__copyPropertiesAct = self.__propertiesEditMenu.addAction(
+            UI.PixmapCache.getIcon("editCopy"),
+            self.tr("Copy"), self.propertiesEdit.copy)
+        self.__propertiesEditMenu.addSeparator()
+        self.__selectAllPropertiesAct = self.__propertiesEditMenu.addAction(
+            UI.PixmapCache.getIcon("editSelectAll"),
+            self.tr("Select All"), self.propertiesEdit.selectAll)
+        self.__propertiesEditMenu.addSeparator()
+        self.__clearPropertiesAct = self.__propertiesEditMenu.addAction(
+            UI.PixmapCache.getIcon("editDelete"),
+            self.tr("Clear"), self.propertiesEdit.clear)
+        
+        self.propertiesEdit.copyAvailable.connect(
+            self.__copyPropertiesAct.setEnabled)
+        
+        self.__copyPropertiesAct.setEnabled(False)
+    
     #######################################################################
     ## Slots handling MQTT related signals
     #######################################################################
     
     @pyqtSlot(dict, int)
-    @pyqtSlot(dict, int, int)
-    def __brokerConnected(self, flags, rc, packetType=None):
+    @pyqtSlot(dict, int, int, dict)
+    def __brokerConnected(self, flags, rc, packetType=None, properties=None):
         """
         Private slot to handle being connected to a broker.
         
@@ -245,6 +282,9 @@
         @type int
         @param packetType packet type as reported by the client
         @type int
+        @param properties dictionary containing the received connection
+            properties
+        @type dict
         """
         self.brokerStatusLabel.hide()
         
@@ -253,13 +293,16 @@
             self.__connectedToBroker = True
             self.__connectionOptions = None
         
-            msg = (
-                mqttReasonCode(rc, packetType)
-                if packetType is not None else
-                mqttConnackMessage(rc)
-            )
+        msg = (
+            mqttReasonCode(rc, packetType)
+            if packetType is not None else
+            mqttConnackMessage(rc)
+        )
         self.__flashBrokerStatusLabel(msg)
         
+        if properties:
+            self.__showProperties("Connect", properties)
+        
         self.connectButton.setEnabled(True)
         if rc == 0:
             self.__connectedToBroker = True
@@ -269,7 +312,11 @@
                 UI.PixmapCache.getIcon("ircDisconnect"))
             
             self.subscribeGroup.setEnabled(True)
+            self.subscribePropertiesButton.setVisible(
+                self.__client.getProtocol() == MqttProtocols.MQTTv5)
             self.unsubscribeGroup.setEnabled(True)
+            self.unsubscribePropertiesButton.setVisible(
+                self.__client.getProtocol() == MqttProtocols.MQTTv5)
             self.publishGroup.setEnabled(True)
             self.brokerStatusButton.setEnabled(True)
             
@@ -325,7 +372,9 @@
         self.__updatePublishTopicComboBox()
         
         self.subscribeGroup.setEnabled(False)
+        self.subscribePropertiesButton.setVisible(False)
         self.unsubscribeGroup.setEnabled(False)
+        self.unsubscribePropertiesButton.setVisible(False)
         self.publishGroup.setEnabled(False)
         self.brokerStatusButton.setEnabled(False)
         
@@ -373,7 +422,6 @@
         else:
             self.logEdit.verticalScrollBar().setValue(scrollbarValue)
     
-    # TODO: add support for MQTT v5 properties
     @pyqtSlot(str, bytes, int, bool)
     @pyqtSlot(str, bytes, int, bool, dict)
     def __messageReceived(self, topic, payload, qos, retain, properties=None):
@@ -406,7 +454,7 @@
         @param mid ID of the published message
         @type int
         """
-        # TODO: check this 'pass' statement
+        # nothing to show for this
         pass
     
     @pyqtSlot(int)
@@ -426,8 +474,8 @@
             self.__updateUnsubscribeTopicComboBox()
             self.__updatePublishTopicComboBox()
     
-    @pyqtSlot(int, list)
-    def __topicSubscribedV5(self, mid, reasonCodes):
+    @pyqtSlot(int, list, dict)
+    def __topicSubscribedV5(self, mid, reasonCodes, properties):
         """
         Private slot to handle being subscribed to topics (MQTT v5).
         
@@ -435,9 +483,16 @@
         @type int
         @param reasonCodes list of reason codes, one for each topic
         @type list of ReasonCodes
+        @param properties dictionary containing the received subscribe
+            properties
+        @type dict
         """
         msg = mqttReasonCode(reasonCodes[0].value, reasonCodes[0].packetType)
         self.__flashBrokerStatusLabel(msg)
+        
+        if properties:
+            self.__showProperties("Subscribe", properties)
+        
         self.__topicSubscribed(mid)
     
     @pyqtSlot(int)
@@ -456,8 +511,8 @@
                 self.__updateUnsubscribeTopicComboBox()
                 self.__updatePublishTopicComboBox()
     
-    @pyqtSlot(int, int, int)
-    def __topicUnsubscribedV5(self, mid, rc, packetType):
+    @pyqtSlot(int, int, int, dict)
+    def __topicUnsubscribedV5(self, mid, rc, packetType, properties):
         """
         Private slot to handle being unsubscribed to topics (MQTT v5).
         
@@ -467,9 +522,16 @@
         @type int
         @param packetType packet type as reported by the client
         @type int
+        @param properties dictionary containing the received subscribe
+            properties
+        @type dict
         """
         msg = mqttReasonCode(rc, packetType)
         self.__flashBrokerStatusLabel(msg)
+        
+        if properties:
+            self.__showProperties("Subscribe", properties)
+        
         self.__topicUnsubscribed(mid)
     
     #######################################################################
@@ -565,6 +627,18 @@
             else:
                 self.__directConnectToBroker()
     
+    @pyqtSlot()
+    def on_subscribePropertiesButton_clicked(self):
+        """
+        Private slot to edit the subscribe user properties.
+        """
+        topic = self.subscribeTopicEdit.text()
+        self.__editProperties(
+            "subscribe",
+            self.tr("SUBSCRIBE: User Properties for '{0}'").format(topic),
+            topic
+        )
+    
     @pyqtSlot(str)
     def on_subscribeTopicEdit_textChanged(self, topic):
         """
@@ -574,6 +648,7 @@
         @type str
         """
         self.subscribeButton.setEnabled(bool(topic))
+        self.subscribePropertiesButton.setEnabled(bool(topic))
     
     @pyqtSlot()
     def on_subscribeTopicEdit_returnPressed(self):
@@ -598,8 +673,27 @@
                     self.tr("Subscriptions to the Status topic '$SYS' shall"
                             " be done on the 'Status' tab."))
             else:
-                self.__topicQueue[
-                    self.__client.subscribe(topic, qos)[1]] = topic
+                properties = (
+                    self.__plugin.getPreferences("SubscribeProperties")
+                    .get(topic, [])
+                    if self.__client.getProtocol() == MqttProtocols.MQTTv5 else
+                    None
+                )
+                result, mid = self.__client.subscribe(
+                    topic, qos=qos, properties=properties)
+                self.__topicQueue[mid] = topic
+    
+    @pyqtSlot()
+    def on_unsubscribePropertiesButton_clicked(self):
+        """
+        Private slot to edit the unsubscribe user properties.
+        """
+        topic = self.unsubscribeTopicComboBox.currentText()
+        self.__editProperties(
+            "unsubscribe",
+            self.tr("UNSUBSCRIBE: User Properties for '{0}'").format(topic),
+            topic
+        )
     
     @pyqtSlot(str)
     def on_unsubscribeTopicComboBox_currentIndexChanged(self, topic):
@@ -610,6 +704,7 @@
         @type str
         """
         self.unsubscribeButton.setEnabled(bool(topic))
+        self.unsubscribePropertiesButton.setEnabled(bool(topic))
     
     @pyqtSlot()
     def on_unsubscribeButton_clicked(self):
@@ -618,8 +713,15 @@
         """
         topic = self.unsubscribeTopicComboBox.currentText()
         if topic:
-            self.__topicQueue[
-                self.__client.unsubscribe(topic)[1]] = topic
+            properties = (
+                self.__plugin.getPreferences("SubscribeProperties")
+                .get(topic, [])
+                if self.__client.getProtocol() == MqttProtocols.MQTTv5 else
+                None
+            )
+            result, mid = self.__client.unsubscribe(
+                topic, properties=properties)
+            self.__topicQueue[mid] = topic
     
     @pyqtSlot(str)
     def on_publishTopicComboBox_editTextChanged(self, topic):
@@ -673,6 +775,19 @@
                 self.on_publishClearButton_clicked()
     
     @pyqtSlot()
+    def on_publishClearRetainedButton_clicked(self):
+        """
+        Private slot to clear the retained messages for the topic.
+        """
+        topic = self.publishTopicComboBox.currentText()
+        
+        msgInfo = self.__client.publish(topic, payload=None, retain=True)
+        if msgInfo.rc == 0:
+            if topic not in self.__publishedTopics:
+                self.__publishedTopics.append(topic)
+            self.__updatePublishTopicComboBox(resetTopic=False)
+    
+    @pyqtSlot()
     def on_publishClearButton_clicked(self):
         """
         Private slot to clear the publish data fields.
@@ -693,6 +808,16 @@
         """
         self.publishPayloadEdit.setEnabled(not bool(path))
     
+    @pyqtSlot(QPoint)
+    def on_propertiesEdit_customContextMenuRequested(self, pos):
+        """
+        Private slot to show the context menu for the properties output.
+        
+        @param pos the position of the mouse pointer
+        @type QPoint
+        """
+        self.__propertiesEditMenu.popup(self.propertiesEdit.mapToGlobal(pos))
+    
     @pyqtSlot()
     def on_brokerStatusButton_clicked(self):
         """
@@ -920,7 +1045,10 @@
         """
         self.unsubscribeTopicComboBox.clear()
         self.unsubscribeTopicComboBox.addItems(sorted(self.__subscribedTopics))
-        self.unsubscribeButton.setEnabled(len(self.__subscribedTopics) > 0)
+        self.unsubscribeButton.setEnabled(
+            bool(self.__subscribedTopics))
+        self.unsubscribePropertiesButton.setEnabled(
+            bool(self.__subscribedTopics))
     
     def __updatePublishTopicComboBox(self, resetTopic=True):
         """
@@ -954,7 +1082,6 @@
         @param properties properties sent with the message (MQTT v5)
         @type dict
         """
-        # TODO: add Output for properties
         scrollbarValue = self.messagesEdit.verticalScrollBar().value()
         
         textCursor = self.messagesEdit.textCursor()
@@ -964,7 +1091,7 @@
             self.messagesEdit.insertPlainText("\n")
         
         textBlockFormat = textCursor.blockFormat()
-        if self.__isAlternate:
+        if self.__isMessageAlternate:
             textBlockFormat.setBackground(
                 self.messagesEdit.palette().alternateBase())
         else:
@@ -984,6 +1111,16 @@
             self.messagesEdit.setCurrentCharFormat(self.__messagesQosFormat)
             self.messagesEdit.insertPlainText(self.tr("Retained Message\n"))
         
+        if properties:
+            self.messagesEdit.setCurrentCharFormat(self.__messagesTopicFormat)
+            self.messagesEdit.insertPlainText(self.tr("Properties:\n"))
+            self.messagesEdit.setCurrentCharFormat(self.__messagesFormat)
+            for name, value in sorted(properties.items):
+                self.messagesEdit.insertPlainText(
+                    self.tr("{0}: {1}\n", "property name, property value")
+                    .format(name, value)
+                )
+        
         payloadStr = str(payload, encoding="utf-8", errors="replace")
         self.messagesEdit.setCurrentCharFormat(self.__messagesFormat)
         self.messagesEdit.insertPlainText(
@@ -994,11 +1131,11 @@
         else:
             self.messagesEdit.verticalScrollBar().setValue(scrollbarValue)
         
-        self.__isAlternate = not self.__isAlternate
+        self.__isMessageAlternate = not self.__isMessageAlternate
     
     def __handleBrokerStatusMessage(self, topic, payload):
         """
-        Private method to append a received message to the output.
+        Private method to handle a status message of the broker.
         
         @param topic topic of the received message
         @type str
@@ -1158,3 +1295,64 @@
             )
             self.__client.connectToServerWithOptions(
                 host, port=port, options=connectionProfile)
+    
+    def __showProperties(self, typeStr, properties):
+        """
+        Private method to display the received properties in the properties
+        pane.
+        
+        @param typeStr message type
+        @type str
+        @param properties dictionary containing the relevant properties
+        @type dict
+        """
+        textCursor = self.propertiesEdit.textCursor()
+        if not self.propertiesEdit.document().isEmpty():
+            textCursor.movePosition(QTextCursor.MoveOperation.End)
+            self.propertiesEdit.setTextCursor(textCursor)
+        
+        textBlockFormat = textCursor.blockFormat()
+        if self.__isPropertiesAlternate:
+            textBlockFormat.setBackground(
+                self.propertiesEdit.palette().alternateBase())
+        else:
+            textBlockFormat.setBackground(
+                self.propertiesEdit.palette().base())
+        textCursor.setBlockFormat(textBlockFormat)
+        textCursor.movePosition(QTextCursor.MoveOperation.End)
+        self.propertiesEdit.setTextCursor(textCursor)
+        
+        self.propertiesEdit.setCurrentCharFormat(self.__propertiesTopicFormat)
+        self.propertiesEdit.insertPlainText(typeStr + "\n")
+        
+        for name, value in sorted(properties.items()):
+            self.propertiesEdit.setCurrentCharFormat(
+                self.__propertiesNameFormat)
+            self.propertiesEdit.insertPlainText("{0}: ".format(name))
+            self.propertiesEdit.setCurrentCharFormat(self.__propertiesFormat)
+            self.propertiesEdit.insertPlainText("{0}\n".format(str(value)))
+        
+        self.propertiesEdit.ensureCursorVisible()
+        
+        self.__isPropertiesAlternate = not self.__isPropertiesAlternate
+    
+    def __editProperties(self, propertiesType, header, key):
+        """
+        Private method to edit user properties of a given type.
+        
+        @param propertiesType properties type (one of 'subscribe',
+            'unsubscribe', 'publish')
+        @type str
+        @param header header to be shown in the edit dialog
+        @type str
+        @param key key to retrieve the right properties
+        @type str
+        """
+        from .MqttUserPropertiesEditor import MqttUserPropertiesEditor
+        
+        preferencesKey = "{0}Properties".format(propertiesType.capitalize())
+        properties = self.__plugin.getPreferences(preferencesKey)
+        dlg = MqttUserPropertiesEditor(header, properties.get(key, []), self)
+        if dlg.exec() == QDialog.DialogCode.Accepted:
+            properties[key] = dlg.getProperties()
+            self.__plugin.setPreferences(preferencesKey, properties)

eric ide

mercurial