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