Wed, 21 Jul 2021 20:10:36 +0200
Started implementing support for MQTT v5 user properties.
diff -r 0eae5f616154 -r 70b8858199f5 MqttMonitor/MqttClient.py --- a/MqttMonitor/MqttClient.py Tue Jul 20 18:10:55 2021 +0200 +++ b/MqttMonitor/MqttClient.py Wed Jul 21 20:10:36 2021 +0200 @@ -15,6 +15,7 @@ import paho.mqtt.client as mqtt from paho.mqtt.packettypes import PacketTypes +from paho.mqtt.properties import Properties from Utilities.crypto import pwConvert @@ -33,22 +34,32 @@ Class implementing a PyQt wrapper around the paho MQTT client. @signal onConnectV3(flags, rc) emitted after the client has connected to - the broker + the broker (MQTT v3) + @signal onConnectV5(flags, rc, packetType, properties emitted after the + client has connected to the broker (MQTT v5) @signal onDisconnectedV3(rc) emitted after the client has disconnected from - the broker + the broker (MQTT v3) + @signal onDisconnectedV5(rc, packetType) emitted after the client has + disconnected from the broker (MQTT v5) @signal onLog(level, message) emitted to send client log data @signal onMessageV3(topic, payload, qos, retain) emitted after a message - has been received by the client + has been received by the client (MQTT v3) + @signal onMessageV5(topic, payload, qos, retain, properties) emitted after + a message has been received by the client (MQTT v5) @signal onPublish(mid) emitted after a message has been published @signal onSubscribeV3(mid, grantedQos) emitted after the client has - subscribed to some topics + subscribed to some topics (MQTT v3) + @signal onSubscribeV5(mid, reasonCodes, properties) emitted after the + client has subscribed to some topics (MQTT v5) @signal onUnsubscribeV3(mid) emitted after the client has unsubscribed from - some topics + some topics (MQTT v3) + @signal onUnsubscribeV5(mid, rc, packetType, properties) emitted after the + client has unsubscribed from some topics (MQTT v5) @signal connectTimeout() emitted to indicate, that a connection attempt timed out """ onConnectV3 = pyqtSignal(dict, int) - onConnectV5 = pyqtSignal(dict, int, int) + onConnectV5 = pyqtSignal(dict, int, int, dict) onDisconnectedV3 = pyqtSignal(int) onDisconnectedV5 = pyqtSignal(int, int) onLog = pyqtSignal(int, str) @@ -56,9 +67,9 @@ onMessageV5 = pyqtSignal(str, bytes, int, bool, dict) onPublish = pyqtSignal(int) onSubscribeV3 = pyqtSignal(int, tuple) - onSubscribeV5 = pyqtSignal(int, list) + onSubscribeV5 = pyqtSignal(int, list, dict) onUnsubscribeV3 = pyqtSignal(int) - onUnsubscribeV5 = pyqtSignal(int, int, int) + onUnsubscribeV5 = pyqtSignal(int, int, int, dict) connectTimeout = pyqtSignal() @@ -151,25 +162,34 @@ message.qos, message.retain) ) else: - # TODO: add properties to signals self.__mqttClient.on_connect = ( lambda client, userdata, flags, rc, properties=None: - self.onConnectV5.emit(flags, rc.value, rc.packetType) + self.onConnectV5.emit( + flags, rc.value, rc.packetType, + properties.json() if properties is not None else {} + ) ) self.__mqttClient.on_disconnect = self.__onDisconnectedV5 self.__mqttClient.on_subscribe = ( lambda client, userdata, mid, reasonCodes, properties=None: - self.onSubscribeV5.emit(mid, reasonCodes) + self.onSubscribeV5.emit( + mid, reasonCodes, + properties.json() if properties is not None else {} + ) ) self.__mqttClient.on_unsubscribe = ( lambda client, userdata, mid, properties, rc: - self.onUnsubscribeV5.emit(mid, rc.value, rc.packetType) + self.onUnsubscribeV5.emit( + mid, rc.value, rc.packetType, + properties.json() if properties is not None else {} + ) ) self.__mqttClient.on_message = ( lambda client, userdata, message: - self.onMessageV5.emit(message.topic, message.payload, - message.qos, message.retain, - message.properties.json()) + self.onMessageV5.emit( + message.topic, message.payload, message.qos, + message.retain, message.properties.json() + ) ) self.__mqttClient.on_log = ( lambda client, userdata, level, buf: @@ -209,22 +229,6 @@ self.stopLoop() self.connectTimeout.emit() -## def reinitialise(self, clientId="", cleanSession=True, userdata=None): -## """ -## Public method to reinitialize the client with given data. -## -## @param clientId ID to be used for the client -## @type str -## @param cleanSession flag indicating to start a clean session -## @type bool -## @param userdata user data -## @type any -## """ -## self.__mqttClient.reinitialise( -## client_id=clientId, clean_session=cleanSession, userdata=userdata) -## -## self.__initCallbacks() -## def setConnectionTimeout(self, timeout): """ Public method to set the connection timeout value. @@ -365,8 +369,6 @@ trying to connect with the given parameters @type bool """ -## if reinit: -## self.reinitialise() # TODO: MQTTv5: add support for MQTTv5 properties self.__mqttClient.connect_async( host, port=port, keepalive=keepalive, bind_address=bindAddress, @@ -445,7 +447,6 @@ self.__cleanSession = parametersDict["CleanSession"] self.connectToServer(host, port=port, keepalive=parametersDict["Keepalive"]) -## reinit=False) else: keepalive = self.defaultConnectionOptions["Keepalive"] self.connectToServer(host, port=port, keepalive=keepalive, @@ -503,9 +504,7 @@ # TODO: MQTTv5: add support for reason code self.__mqttClient.disconnect() - # TODO: MQTTv5: add support for properties - # TODO: MQTTv5: add support for subscribe options - def subscribe(self, topic, qos=0): + def subscribe(self, topic, qos=0, properties=None): """ Public method to subscribe to topics with quality of service. @@ -514,22 +513,37 @@ @type str or tuple of (str, int) or list of tuple of (str, int) @param qos quality of service @type int, one of 0, 1 or 2 + @param properties list of user properties to be sent with the + subscription + @type list of tuple of (str, str) @return tuple containing the result code and the message ID @rtype tuple of (int, int) """ - return self.__mqttClient.subscribe(topic, qos=qos) + props = ( + self.__createPropertiesObject(PacketTypes.SUBSCRIBE, properties) + if properties else + None + ) + return self.__mqttClient.subscribe(topic, qos=qos, properties=props) - # TODO: MQTTv5: add support for properties (?) - def unsubscribe(self, topic): + def unsubscribe(self, topic, properties=None): """ Public method to unsubscribe topics. @param topic topic or list of topics to unsubscribe @type str or list of str + @param properties list of user properties to be sent with the + subscription + @type list of tuple of (str, str) @return tuple containing the result code and the message ID @rtype tuple of (int, int) """ - return self.__mqttClient.unsubscribe(topic) + props = ( + self.__createPropertiesObject(PacketTypes.SUBSCRIBE, properties) + if properties else + None + ) + return self.__mqttClient.unsubscribe(topic, properties=props) # TODO: MQTTv5: add support for properties def publish(self, topic, payload=None, qos=0, retain=False): @@ -550,6 +564,22 @@ """ return self.__mqttClient.publish(topic, payload=payload, qos=qos, retain=retain) + + def __createPropertiesObject(self, packetType, properties): + """ + Private method to assemble the MQTT v5 properties object. + + @param packetType type of the MQTT packet + @type PacketTypes (= int) + @param properties list of user properties + @type list of tuple of (str, str) + @return MQTT v5 properties object + @rtype Properties + """ + props = Properties(packetType) + for userProperty in properties: + props.UserProperty = tuple(userProperty) + return props def mqttConnackMessage(connackCode):
diff -r 0eae5f616154 -r 70b8858199f5 MqttMonitor/MqttMonitorWidget.py --- 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)
diff -r 0eae5f616154 -r 70b8858199f5 MqttMonitor/MqttMonitorWidget.ui --- a/MqttMonitor/MqttMonitorWidget.ui Tue Jul 20 18:10:55 2021 +0200 +++ b/MqttMonitor/MqttMonitorWidget.ui Wed Jul 21 20:10:36 2021 +0200 @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>400</width> - <height>600</height> + <height>715</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout"> @@ -115,23 +115,30 @@ </layout> </item> <item row="0" column="2"> - <widget class="QToolButton" name="brokerConnectionOptionsButton"> - <property name="toolTip"> - <string>Press to open a dialog to enter connection options</string> + <layout class="QHBoxLayout" name="horizontalLayout_19"> + <property name="spacing"> + <number>0</number> </property> - </widget> + <item> + <widget class="QToolButton" name="brokerConnectionOptionsButton"> + <property name="toolTip"> + <string>Press to open a dialog to enter connection options</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="connectButton"> + <property name="toolTip"> + <string>Press to connect to/disconnect from the broker</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> </item> - <item row="0" column="3"> - <widget class="QToolButton" name="connectButton"> - <property name="toolTip"> - <string>Press to connect to/disconnect from the broker</string> - </property> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item row="1" column="0" colspan="4"> + <item row="1" column="0" colspan="3"> <widget class="QLabel" name="brokerStatusLabel"> <property name="wordWrap"> <bool>true</bool> @@ -150,7 +157,7 @@ <attribute name="title"> <string>Pub/Sub</string> </attribute> - <layout class="QVBoxLayout" name="verticalLayout_5"> + <layout class="QVBoxLayout" name="verticalLayout_7"> <item> <widget class="QGroupBox" name="subscribeGroup"> <property name="enabled"> @@ -159,7 +166,7 @@ <property name="title"> <string>Subscribe</string> </property> - <layout class="QHBoxLayout" name="horizontalLayout_2"> + <layout class="QHBoxLayout" name="horizontalLayout_20"> <item> <widget class="QLabel" name="label"> <property name="text"> @@ -198,11 +205,28 @@ </widget> </item> <item> - <widget class="QToolButton" name="subscribeButton"> - <property name="toolTip"> - <string>Press to subscribe to the given topic</string> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="spacing"> + <number>0</number> </property> - </widget> + <item> + <widget class="QToolButton" name="subscribeButton"> + <property name="toolTip"> + <string>Press to subscribe to the given topic</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="subscribePropertiesButton"> + <property name="toolTip"> + <string>Press to edit the user properties</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> </item> </layout> </widget> @@ -215,7 +239,7 @@ <property name="title"> <string>Unsubscribe</string> </property> - <layout class="QHBoxLayout" name="horizontalLayout_3"> + <layout class="QHBoxLayout" name="horizontalLayout_21"> <item> <widget class="QLabel" name="label_4"> <property name="text"> @@ -237,11 +261,28 @@ </widget> </item> <item> - <widget class="QToolButton" name="unsubscribeButton"> - <property name="toolTip"> - <string>Press to unsubscribe the selected topic</string> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="spacing"> + <number>0</number> </property> - </widget> + <item> + <widget class="QToolButton" name="unsubscribeButton"> + <property name="toolTip"> + <string>Press to unsubscribe the selected topic</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="unsubscribePropertiesButton"> + <property name="toolTip"> + <string>Press to edit the user properties</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> </item> </layout> </widget> @@ -368,6 +409,16 @@ </widget> </item> <item> + <widget class="QPushButton" name="publishClearRetainedButton"> + <property name="toolTip"> + <string>Press to clear the retained messages of the selected topic</string> + </property> + <property name="text"> + <string>Clear Retained</string> + </property> + </widget> + </item> + <item> <spacer name="horizontalSpacer_3"> <property name="orientation"> <enum>Qt::Horizontal</enum> @@ -403,17 +454,32 @@ </widget> </item> <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>MQTT V5 Properties</string> </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QPlainTextEdit" name="propertiesEdit"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="tabChangesFocus"> + <bool>true</bool> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </widget> </item> </layout> </widget> @@ -548,7 +614,7 @@ <rect> <x>0</x> <y>0</y> - <width>178</width> + <width>344</width> <height>840</height> </rect> </property> @@ -1414,7 +1480,9 @@ <tabstop>publishRetainCheckBox</tabstop> <tabstop>publishButton</tabstop> <tabstop>publishClearButton</tabstop> + <tabstop>publishClearRetainedButton</tabstop> <tabstop>clearPublishCheckBox</tabstop> + <tabstop>propertiesEdit</tabstop> <tabstop>messagesSearchWidget</tabstop> <tabstop>messagesEdit</tabstop> <tabstop>saveMessagesButton</tabstop>
diff -r 0eae5f616154 -r 70b8858199f5 MqttMonitor/MqttReasonCodes.py --- a/MqttMonitor/MqttReasonCodes.py Tue Jul 20 18:10:55 2021 +0200 +++ b/MqttMonitor/MqttReasonCodes.py Wed Jul 21 20:10:36 2021 +0200 @@ -205,6 +205,7 @@ ): [PacketTypes.SUBACK, PacketTypes.DISCONNECT]}, } + def mqttReasonCode(rc, packetType): """ Function to get the readable reason code string given the result code and @@ -214,6 +215,8 @@ @type int @param packetType packet type @type PacketTypes (= int) + @return message associated with the reason code and packet type + @rtype str """ if rc not in MqttReasonCodeNames: return QCoreApplication.translate(
diff -r 0eae5f616154 -r 70b8858199f5 MqttMonitor/MqttUserPropertiesEditor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MqttMonitor/MqttUserPropertiesEditor.py Wed Jul 21 20:10:36 2021 +0200 @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an editor for MQTT v5 user properties. +""" + +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtWidgets import QDialog, QTableWidgetItem + +from .Ui_MqttUserPropertiesEditor import Ui_MqttUserPropertiesEditor + +import UI.PixmapCache + + +class MqttUserPropertiesEditor(QDialog, Ui_MqttUserPropertiesEditor): + """ + Class implementing an editor for MQTT v5 user properties. + """ + def __init__(self, header, properties, parent=None): + """ + Constructor + + @param header text to be shown in the dialog header label + @type str + @param properties list of defined user properties + @type list of tuple of (str, str) + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + self.addButton.setIcon(UI.PixmapCache.getIcon("plus")) + self.deleteButton.setIcon(UI.PixmapCache.getIcon("minus")) + self.clearButton.setIcon(UI.PixmapCache.getIcon("editDelete")) + + self.headerLabel.setText(header) + + if properties: + self.propertiesTable.setRowCount(len(properties)) + for row, (key, value) in enumerate(properties): + self.propertiesTable.setItem(row, 0, QTableWidgetItem(key)) + self.propertiesTable.setItem(row, 1, QTableWidgetItem(value)) + + self.deleteButton.setEnabled(False) + + @pyqtSlot() + def on_propertiesTable_itemSelectionChanged(self): + """ + Private slot to handle the selection of rows. + """ + self.deleteButton.setEnabled( + bool(self.propertiesTable.selectedItems())) + + @pyqtSlot() + def on_addButton_clicked(self): + """ + Private slot to add a row to the table. + """ + self.propertiesTable.setRowCount(self.propertiesTable.rowCount() + 1) + self.propertiesTable.setCurrentCell( + self.propertiesTable.rowCount() - 1, 0) + + @pyqtSlot() + def on_deleteButton_clicked(self): + """ + Private slot to delete the selected rows. + """ + selectedRanges = self.propertiesTable.selectedRanges() + selectedRows = [(r.bottomRow(), r.topRow()) for r in selectedRanges] + for bottomRow, topRow in sorted(selectedRows, reverse=True): + for row in range(bottomRow, topRow - 1, -1): + self.propertiesTable.removeRow(row) + + @pyqtSlot() + def on_clearButton_clicked(self): + """ + Private slot to delete all properties. + """ + self.propertiesTable.clearContents() + self.propertiesTable.setRowCount(10) + self.propertiesTable.setCurrentCell(0, 0) + + def getProperties(self): + """ + Public method to get the list of defined user properties. + + @return list of defined user properties + @rtype list of tuple of (str, str) + """ + properties = [] + + for row in range(self.propertiesTable.rowCount()): + keyItem = self.propertiesTable.item(row, 0) + key = keyItem.text() if keyItem else "" + if key: + valueItem = self.propertiesTable.item(row, 1) + value = valueItem.text() if valueItem else "" + properties.append((key, value)) + + return properties
diff -r 0eae5f616154 -r 70b8858199f5 MqttMonitor/MqttUserPropertiesEditor.ui --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MqttMonitor/MqttUserPropertiesEditor.ui Wed Jul 21 20:10:36 2021 +0200 @@ -0,0 +1,163 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MqttUserPropertiesEditor</class> + <widget class="QDialog" name="MqttUserPropertiesEditor"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>350</height> + </rect> + </property> + <property name="windowTitle"> + <string>User Properties</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="headerLabel"> + <property name="text"> + <string/> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QTableWidget" name="propertiesTable"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="rowCount"> + <number>10</number> + </property> + <property name="columnCount"> + <number>2</number> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <row/> + <row/> + <row/> + <row/> + <row/> + <row/> + <row/> + <row/> + <row/> + <row/> + <column> + <property name="text"> + <string>Key</string> + </property> + </column> + <column> + <property name="text"> + <string>Value</string> + </property> + </column> + </widget> + </item> + <item row="1" column="1"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QToolButton" name="addButton"> + <property name="toolTip"> + <string>Press to add a new empty row</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="deleteButton"> + <property name="toolTip"> + <string>Press to delete the selected properties</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="clearButton"> + <property name="toolTip"> + <string>Press to delete all properties</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="2" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>MqttUserPropertiesEditor</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>MqttUserPropertiesEditor</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
diff -r 0eae5f616154 -r 70b8858199f5 PluginMqttMonitor.epj --- a/PluginMqttMonitor.epj Tue Jul 20 18:10:55 2021 +0200 +++ b/PluginMqttMonitor.epj Wed Jul 21 20:10:36 2021 +0200 @@ -154,7 +154,8 @@ "FORMS": [ "MqttMonitor/MqttConnectionOptionsDialog.ui", "MqttMonitor/MqttConnectionProfilesDialog.ui", - "MqttMonitor/MqttMonitorWidget.ui" + "MqttMonitor/MqttMonitorWidget.ui", + "MqttMonitor/MqttUserPropertiesEditor.ui" ], "HASH": "8b864e3e4a3495e242eae3cb3ef4dc8522bf6ce7", "IDLPARAMS": { @@ -214,7 +215,8 @@ "MqttMonitor/__init__.py", "PluginMqttMonitor.py", "__init__.py", - "MqttMonitor/MqttReasonCodes.py" + "MqttMonitor/MqttReasonCodes.py", + "MqttMonitor/MqttUserPropertiesEditor.py" ], "SPELLEXCLUDES": "", "SPELLLANGUAGE": "en", @@ -280,4 +282,4 @@ "VCSOTHERDATA": {}, "VERSION": "" } -} \ No newline at end of file +}
diff -r 0eae5f616154 -r 70b8858199f5 PluginMqttMonitor.py --- a/PluginMqttMonitor.py Tue Jul 20 18:10:55 2021 +0200 +++ b/PluginMqttMonitor.py Wed Jul 21 20:10:36 2021 +0200 @@ -95,6 +95,10 @@ "BrokerProfiles": "{}", # JSON formatted empty dict # __IGNORE_WARNING_M613__ "MostRecentProfile": "", # most recently used profile + "SubscribeProperties": "{}", # JSON formatted empty dict + # __IGNORE_WARNING_M613__ + "UnsubscribeProperties": "{}", # JSON formatted empty dict + # __IGNORE_WARNING_M613__ } self.__translator = None @@ -212,7 +216,8 @@ @return value of the requested setting @rtype Any """ - if key in ["RecentBrokersWithPort", "BrokerProfiles"]: + if key in ["RecentBrokersWithPort", "BrokerProfiles", + "SubscribeProperties", "UnsubscribeProperties"]: return json.loads(Preferences.Prefs.settings.value( self.PreferencesKey + "/" + key, self.__defaults[key])) else: @@ -228,7 +233,8 @@ @param value value to be set @type Any """ - if key in ["RecentBrokersWithPort", "BrokerProfiles"]: + if key in ["RecentBrokersWithPort", "BrokerProfiles", + "SubscribeProperties", "UnsubscribeProperties"]: Preferences.Prefs.settings.setValue( self.PreferencesKey + "/" + key, json.dumps(value)) else: