Thu, 22 Jul 2021 19:02:32 +0200
Continued implementing support for MQTT v5 user properties.
--- a/MqttMonitor/MqttClient.py Wed Jul 21 20:10:36 2021 +0200 +++ b/MqttMonitor/MqttClient.py Thu Jul 22 19:02:32 2021 +0200 @@ -122,6 +122,7 @@ self.__cleanSession = cleanSession self.__protocol = protocol + self.__disconnectUserProperties = [] if protocol == MqttProtocols.MQTTv5: cleanSession = None @@ -278,7 +279,7 @@ """ self.__mqttClient.user_data_set(userdata) - # TODO: MQTTv5: add support for properties + # TODO: MQTTv5: add support for WILL properties def setLastWill(self, topic, payload=None, qos=0, retain=False): """ Public method to set the last will of the client. @@ -350,7 +351,7 @@ self.__loopStarted = False def connectToServer(self, host, port=1883, keepalive=60, bindAddress="", - reinit=True): + properties=None): """ Public method to connect to a remote MQTT broker. @@ -365,14 +366,18 @@ @param bindAddress IP address of a local network interface to bind this client to @type str - @param reinit flag indicating to reinitialize the MQTT client before - trying to connect with the given parameters - @type bool + @param properties list of user properties to be sent with the + subscription + @type list of tuple of (str, str) """ - # TODO: MQTTv5: add support for MQTTv5 properties + props = ( + self.__createPropertiesObject(PacketTypes.CONNECT, properties) + if properties else + None + ) self.__mqttClient.connect_async( host, port=port, keepalive=keepalive, bind_address=bindAddress, - clean_start=self.__cleanSession) + clean_start=self.__cleanSession, properties=props) self.__connectTimeoutTimer.start() @@ -393,10 +398,10 @@ this client to @type str @param options dictionary containing the connection options. This - dictionary should contain the keys "ClientId", "Keepalive", - "CleanSession", "Username", "Password", "WillTopic", "WillMessage", - "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", "TlsClientCert", - "TlsClientKey", "ConnectionTimeout" + dictionary should contain the keys "ClientId", "ConnectionTimeout", + "Keepalive", "CleanSession", "Username", "Password", "WillTopic", + "WillMessage", "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", + "TlsClientCert", "TlsClientKey", "UserProperties". @type dict """ if options: @@ -443,10 +448,25 @@ # use default TLS configuration self.setTLS() + # step 4: get the connect user properties + if self.__protocol == MqttProtocols.MQTTv5: + try: + userProperties = parametersDict["UserProperties"] + properties = userProperties["connect"][:] + self.__disconnectUserProperties = ( + userProperties["connect"][:] + if userProperties["use_connect"] else + userProperties["disconnect"][:] + ) + except KeyError: + properties = None + else: + properties = None # step 4: connect to server self.__cleanSession = parametersDict["CleanSession"] self.connectToServer(host, port=port, - keepalive=parametersDict["Keepalive"]) + keepalive=parametersDict["Keepalive"], + properties=properties) else: keepalive = self.defaultConnectionOptions["Keepalive"] self.connectToServer(host, port=port, keepalive=keepalive, @@ -462,7 +482,7 @@ the keys "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", "TlsClientCert", - "TlsClientKey". + "TlsClientKey", "UserProperties". @rtype dict """ return { @@ -481,6 +501,11 @@ "TlsCaCert": "", "TlsClientCert": "", "TlsClientKey": "", + "UserProperties": { + "connect": [], + "disconnect": [], + "use_connect": True, + }, } def reconnectToServer(self): @@ -500,9 +525,13 @@ """ self.__connectTimeoutTimer.stop() - # TODO: MQTTv5: add support for properties (?) - # TODO: MQTTv5: add support for reason code - self.__mqttClient.disconnect() + props = ( + self.__createPropertiesObject( + PacketTypes.DISCONNECT, self.__disconnectUserProperties) + if self.__disconnectUserProperties else + None + ) + self.__mqttClient.disconnect(properties=props) def subscribe(self, topic, qos=0, properties=None): """ @@ -539,14 +568,14 @@ @rtype tuple of (int, int) """ props = ( - self.__createPropertiesObject(PacketTypes.SUBSCRIBE, properties) + self.__createPropertiesObject(PacketTypes.UNSUBSCRIBE, 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): + def publish(self, topic, payload=None, qos=0, retain=False, + properties=None): """ Public method to publish to a topic. @@ -559,11 +588,19 @@ @param retain flag indicating to set as the "last known good"/retained message for the topic @type bool + @param properties list of user properties to be sent with the + subscription + @type list of tuple of (str, str) @return message info object @rtype mqtt.MQTTMessageInfo """ + props = ( + self.__createPropertiesObject(PacketTypes.PUBLISH, properties) + if properties else + None + ) return self.__mqttClient.publish(topic, payload=payload, qos=qos, - retain=retain) + retain=retain, properties=props) def __createPropertiesObject(self, packetType, properties): """ @@ -577,8 +614,7 @@ @rtype Properties """ props = Properties(packetType) - for userProperty in properties: - props.UserProperty = tuple(userProperty) + props.UserProperty = properties return props
--- a/MqttMonitor/MqttConnectionOptionsDialog.py Wed Jul 21 20:10:36 2021 +0200 +++ b/MqttMonitor/MqttConnectionOptionsDialog.py Thu Jul 22 19:02:32 2021 +0200 @@ -7,6 +7,8 @@ Module implementing a dialog to enter MQTT connection options. """ +import copy + from PyQt6.QtCore import pyqtSlot, QUuid from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton @@ -24,6 +26,7 @@ """ Class implementing a dialog to enter MQTT connection options. """ + # TODO: add WILL user properties def __init__(self, options=None, parent=None): """ Constructor @@ -32,7 +35,7 @@ populate the dialog with. It must have the keys "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", "WillQos", - "WillRetain", "TlsEnable", "TlsCaCert". + "WillRetain", "TlsEnable", "TlsCaCert", "UserProperties". @type dict @param parent reference to the parent widget @type QWidget @@ -44,8 +47,16 @@ self.tlsCertsFilePicker.setFilters( self.tr("Certificate Files (*.crt *.pem);;All Files (*)")) + # initialize MQTTv5 related stuff + self.on_mqttv5Button_toggled(False) + self.__populateDefaults(options=options) + self.connectPropertiesButton.clicked[bool].connect( + self.__propertiesTypeSelected) + self.disconnectPropertiesButton.clicked[bool].connect( + self.__propertiesTypeSelected) + self.__updateOkButton() def __updateOkButton(self): @@ -76,6 +87,40 @@ self.clientIdEdit.setText( uuid.toString(QUuid.StringFormat.WithoutBraces)) + @pyqtSlot(str) + def on_clientIdEdit_textChanged(self, clientId): + """ + Private slot handling a change of the client ID string. + + @param clientId client ID + @type str + """ + self.__updateOkButton() + + @pyqtSlot(bool) + def on_cleanSessionCheckBox_clicked(self, checked): + """ + Private slot to handle a change of the clean session selection. + + @param checked current state of the clean session selection + @type bool + """ + self.__updateOkButton() + + @pyqtSlot(bool) + def on_mqttv5Button_toggled(self, checked): + """ + Private slot to handle the selection of the MQTT protocol. + + @param checked state of the button + @type bool + """ + self.optionsWidget.setTabEnabled( + self.optionsWidget.indexOf(self.propertiesTab), + checked + ) + # TODO: add code to enable the WILL properties button + @pyqtSlot(QAbstractButton) def on_buttonBox_clicked(self, button): """ @@ -89,6 +134,40 @@ ): self.__populateDefaults(options=None) + @pyqtSlot(bool) + def on_samePropertiesCheckBox_toggled(self, checked): + """ + Private slot to handle a change of the properties usage. + + @param checked flag indicating to use the same user properties for + CONNECT and DISCONNECT + @type bool + """ + if checked and not self.connectPropertiesButton.isChecked(): + self.connectPropertiesButton.click() + self.disconnectPropertiesButton.setEnabled(not checked) + + @pyqtSlot(bool) + def __propertiesTypeSelected(self, checked): + """ + Private slot to handle the switching of the user properties type. + + @param checked state of the buttons + @type bool + """ + if checked: + # handle the selection only + if self.connectPropertiesButton.isChecked(): + self.__userProperties["disconnect"] = ( + self.propertiesWidget.getProperties()) + self.propertiesWidget.setProperties( + self.__userProperties["connect"]) + else: + self.__userProperties["connect"] = ( + self.propertiesWidget.getProperties()) + self.propertiesWidget.setProperties( + self.__userProperties["disconnect"]) + def __populateDefaults(self, options=None): """ Private method to populate the dialog. @@ -97,10 +176,10 @@ default values. @param options dictionary containing the connection options to populate - the dialog with. It must have the keys "ClientId", "Keepalive", - "CleanSession", "Username", "Password", "WillTopic", "WillMessage", - "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", - "ConnectionTimeout". + the dialog with. It must have the keys "ClientId", "Protocol", + "ConnectionTimeout", "Keepalive", "CleanSession", "Username", + "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain", + "TlsEnable", "TlsCaCert", "UserProperties". @type dict """ if options is None: @@ -131,6 +210,27 @@ # TLS parameters self.tlsEnableCheckBox.setChecked(options["TlsEnable"]) self.tlsCertsFilePicker.setText(options["TlsCaCert"]) + + # user properties + self.__userProperties = copy.deepcopy( + options.get("UserProperties", {})) + if not self.__userProperties: + self.__userProperties = { + "connect": [], + "disconnect": [], + "use_connect": True, + } + + if options["Protocol"] == MqttProtocols.MQTTv5: + self.connectPropertiesButton.setChecked(True) + self.propertiesWidget.setProperties( + self.__userProperties["connect"]) + self.samePropertiesCheckBox.setChecked( + self.__userProperties["use_connect"]) + self.disconnectPropertiesButton.setEnabled( + not self.__userProperties["use_connect"]) + else: + self.propertiesWidget.clear() def getConnectionOptions(self): """ @@ -139,7 +239,8 @@ @return dictionary containing the connection options. It has the keys "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", - "WillQos", "WillRetain", "TlsEnable", "TlsCaCert". + "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", + "UserProperties". @rtype dict """ if self.mqttv31Button.isChecked(): @@ -151,6 +252,18 @@ else: protocol = MqttProtocols.MQTTv311 + if protocol == MqttProtocols.MQTTv5: + if self.connectPropertiesButton.isChecked(): + self.__userProperties["connect"] = ( + self.propertiesWidget.getProperties()) + else: + self.__userProperties["disconnect"] = ( + self.propertiesWidget.getProperties()) + self.__userProperties["use_connect"] = ( + self.samePropertiesCheckBox.isChecked()) + else: + self.__userProperties = {} + return { "ClientId": self.clientIdEdit.text(), "Protocol": protocol, @@ -164,25 +277,6 @@ "WillQos": self.willQosSpinBox.value(), "WillRetain": self.willRetainCheckBox.isChecked(), "TlsEnable": self.tlsEnableCheckBox.isChecked(), - "TlsCaCert": self.tlsCertsFilePicker.text() + "TlsCaCert": self.tlsCertsFilePicker.text(), + "UserProperties": copy.deepcopy(self.__userProperties), } - - @pyqtSlot(str) - def on_clientIdEdit_textChanged(self, clientId): - """ - Private slot handling a change of the client ID string. - - @param clientId client ID - @type str - """ - self.__updateOkButton() - - @pyqtSlot(bool) - def on_cleanSessionCheckBox_clicked(self, checked): - """ - Private slot to handle a change of the clean session selection. - - @param checked current state of the clean session selection - @type bool - """ - self.__updateOkButton()
--- a/MqttMonitor/MqttConnectionOptionsDialog.ui Wed Jul 21 20:10:36 2021 +0200 +++ b/MqttMonitor/MqttConnectionOptionsDialog.ui Thu Jul 22 19:02:32 2021 +0200 @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>550</width> - <height>675</height> + <width>450</width> + <height>350</height> </rect> </property> <property name="windowTitle"> @@ -16,67 +16,390 @@ <property name="sizeGripEnabled"> <bool>true</bool> </property> - <layout class="QVBoxLayout" name="verticalLayout_2"> + <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QGroupBox" name="groupBox"> - <property name="title"> - <string>General</string> + <widget class="QTabWidget" name="optionsWidget"> + <property name="currentIndex"> + <number>0</number> </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Client ID:</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="clientIdEdit"> - <property name="toolTip"> - <string>Enter the ID string for this client</string> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="generateIdButton"> - <property name="toolTip"> - <string>Press to generate a client ID</string> - </property> - <property name="text"> - <string>Generate</string> - </property> - </widget> - </item> - </layout> - </item> - <item> - <widget class="QGroupBox" name="groupBox_5"> - <property name="title"> - <string>MQTT Protocol</string> - </property> - <layout class="QHBoxLayout" name="horizontalLayout_3"> + <widget class="QWidget" name="generalTab"> + <attribute name="title"> + <string>General</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> <item> - <widget class="QRadioButton" name="mqttv31Button"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Client ID:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="clientIdEdit"> <property name="toolTip"> - <string>Select to use the MQTT 3.1 protocol</string> + <string>Enter the ID string for this client</string> </property> - <property name="text"> - <string>v 3.1</string> + <property name="clearButtonEnabled"> + <bool>true</bool> </property> </widget> </item> <item> - <widget class="QRadioButton" name="mqttv311Button"> + <widget class="QPushButton" name="generateIdButton"> <property name="toolTip"> - <string>Select to use the MQTT 3.1.1 protocol</string> + <string>Press to generate a client ID</string> </property> <property name="text"> - <string>v 3.1.1</string> + <string>Generate</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="groupBox_5"> + <property name="title"> + <string>MQTT Protocol</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QRadioButton" name="mqttv31Button"> + <property name="toolTip"> + <string>Select to use the MQTT 3.1 protocol</string> + </property> + <property name="text"> + <string>v 3.1</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="mqttv311Button"> + <property name="toolTip"> + <string>Select to use the MQTT 3.1.1 protocol</string> + </property> + <property name="text"> + <string>v 3.1.1</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="mqttv5Button"> + <property name="toolTip"> + <string>Select to use the MQTT 5.0 protocol</string> + </property> + <property name="text"> + <string>v 5.0</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Connection Timeout:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="connectionTimeoutSpinBox"> + <property name="toolTip"> + <string>Enter the connection timeout in seconds</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="maximum"> + <number>300</number> + </property> + <property name="singleStep"> + <number>5</number> + </property> + <property name="value"> + <number>15</number> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>148</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Keep Alive Interval:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="keepaliveSpinBox"> + <property name="toolTip"> + <string>Enter the keep alive interval in seconds</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="maximum"> + <number>300</number> + </property> + <property name="singleStep"> + <number>5</number> + </property> + <property name="value"> + <number>60</number> + </property> + </widget> + </item> + <item row="1" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>148</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QCheckBox" name="cleanSessionCheckBox"> + <property name="toolTip"> + <string>Select to start with a clean session</string> + </property> + <property name="text"> + <string>Clean Session</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>92</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="credentialsTab"> + <attribute name="title"> + <string>User Credentials</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_5"> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>User Name:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="usernameEdit"> + <property name="toolTip"> + <string>Enter the user name</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Password:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="passwordEdit"> + <property name="toolTip"> + <string>Enter the password</string> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="1"> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>204</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="lastWillTab"> + <attribute name="title"> + <string>Last Will</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="1"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>QoS:</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="4"> + <widget class="QPlainTextEdit" name="willMessageEdit"> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>300</height> + </size> + </property> + <property name="toolTip"> + <string>Enter the last will message to be sent</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QSpinBox" name="willQosSpinBox"> + <property name="toolTip"> + <string>Enter the desired QoS value</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="maximum"> + <number>2</number> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QCheckBox" name="willRetainCheckBox"> + <property name="toolTip"> + <string>Select to retain the last will message</string> + </property> + <property name="text"> + <string>Retain</string> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLineEdit" name="willTopicEdit"> + <property name="toolTip"> + <string>Enter the topic of the last will</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="tlsTab"> + <attribute name="title"> + <string>SSL/TLS</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0" colspan="2"> + <widget class="QCheckBox" name="tlsEnableCheckBox"> + <property name="toolTip"> + <string>Select to enable SSL/TLS connections</string> + </property> + <property name="text"> + <string>SSL/TLS Enabled</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>CA File:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="EricPathPicker" name="tlsCertsFilePicker" native="true"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the full path to the CA certificate file; leave empty to use platform default</string> + </property> + </widget> + </item> + <item row="2" column="0" colspan="2"> + <spacer name="verticalSpacer_4"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>214</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <widget class="QWidget" name="propertiesTab"> + <attribute name="title"> + <string>User Properties</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QRadioButton" name="connectPropertiesButton"> + <property name="toolTip"> + <string>Select to edit the CONNECT user properties</string> + </property> + <property name="text"> + <string>CONNECT</string> </property> <property name="checked"> <bool>true</bool> @@ -84,270 +407,42 @@ </widget> </item> <item> - <widget class="QRadioButton" name="mqttv5Button"> + <widget class="QRadioButton" name="disconnectPropertiesButton"> <property name="toolTip"> - <string>Select to use the MQTT 5.0 protocol</string> + <string>Select to edit the DISCONNECT user properties</string> </property> <property name="text"> - <string>v 5.0</string> + <string>DISCONNECT</string> </property> </widget> </item> </layout> - </widget> - </item> - <item> - <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="label_6"> - <property name="text"> - <string>Connection Timeout:</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QSpinBox" name="connectionTimeoutSpinBox"> - <property name="toolTip"> - <string>Enter the connection timeout in seconds</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - <property name="suffix"> - <string> s</string> - </property> - <property name="maximum"> - <number>300</number> - </property> - <property name="singleStep"> - <number>5</number> - </property> - <property name="value"> - <number>15</number> - </property> - </widget> - </item> - <item row="0" column="2"> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>148</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Keep Alive Interval:</string> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QSpinBox" name="keepaliveSpinBox"> - <property name="toolTip"> - <string>Enter the keep alive interval in seconds</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - <property name="suffix"> - <string> s</string> - </property> - <property name="maximum"> - <number>300</number> - </property> - <property name="singleStep"> - <number>5</number> - </property> - <property name="value"> - <number>60</number> - </property> - </widget> - </item> - <item row="1" column="2"> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>148</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item> - <widget class="QCheckBox" name="cleanSessionCheckBox"> - <property name="toolTip"> - <string>Select to start with a clean session</string> - </property> - <property name="text"> - <string>Clean Session</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_2"> - <property name="title"> - <string>User Credentials</string> - </property> - <layout class="QGridLayout" name="gridLayout_2"> - <item row="0" column="0"> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string>User Name:</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="usernameEdit"> - <property name="toolTip"> - <string>Enter the user name</string> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_4"> - <property name="text"> - <string>Password:</string> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="QLineEdit" name="passwordEdit"> - <property name="toolTip"> - <string>Enter the password</string> - </property> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_3"> - <property name="title"> - <string>Last Will and Testament</string> - </property> - <layout class="QGridLayout" name="gridLayout_3"> - <item row="0" column="0"> - <widget class="QLineEdit" name="willTopicEdit"> - <property name="toolTip"> - <string>Enter the topic of the last will</string> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QLabel" name="label_5"> - <property name="text"> - <string>QoS:</string> - </property> - </widget> - </item> - <item row="0" column="2"> - <widget class="QSpinBox" name="willQosSpinBox"> - <property name="toolTip"> - <string>Enter the desired QoS value</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - <property name="maximum"> - <number>2</number> - </property> - </widget> - </item> - <item row="0" column="3"> - <widget class="QCheckBox" name="willRetainCheckBox"> - <property name="toolTip"> - <string>Select to retain the last will message</string> - </property> - <property name="text"> - <string>Retain</string> - </property> - </widget> - </item> - <item row="1" column="0" colspan="4"> - <widget class="QPlainTextEdit" name="willMessageEdit"> - <property name="maximumSize"> - <size> - <width>16777215</width> - <height>300</height> - </size> - </property> - <property name="toolTip"> - <string>Enter the last will message to be sent</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_4"> - <property name="title"> - <string>SSL/TLS</string> - </property> - <layout class="QGridLayout" name="gridLayout_4"> - <item row="0" column="0" colspan="2"> - <widget class="QCheckBox" name="tlsEnableCheckBox"> - <property name="toolTip"> - <string>Select to enable SSL/TLS connections</string> - </property> - <property name="text"> - <string>SSL/TLS Enabled</string> - </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_8"> - <property name="text"> - <string>CA File:</string> - </property> - </widget> - </item> - <item row="1" column="1"> - <widget class="EricPathPicker" name="tlsCertsFilePicker" native="true"> - <property name="enabled"> - <bool>false</bool> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="focusPolicy"> - <enum>Qt::StrongFocus</enum> - </property> - <property name="toolTip"> - <string>Enter the full path to the CA certificate file; leave empty to use platform default</string> - </property> - </widget> - </item> - </layout> + </item> + <item> + <widget class="MqttUserPropertiesEditor" name="propertiesWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="samePropertiesCheckBox"> + <property name="toolTip"> + <string>Select to use the CONNECT user properties when disconnecting</string> + </property> + <property name="text"> + <string>Use CONNECT properties for DISCONNECT</string> + </property> + </widget> + </item> + </layout> + </widget> </widget> </item> <item> @@ -369,8 +464,15 @@ <header>EricWidgets/EricPathPicker.h</header> <container>1</container> </customwidget> + <customwidget> + <class>MqttUserPropertiesEditor</class> + <extends>QWidget</extends> + <header>MqttMonitor/MqttUserPropertiesEditor.h</header> + <container>1</container> + </customwidget> </customwidgets> <tabstops> + <tabstop>optionsWidget</tabstop> <tabstop>clientIdEdit</tabstop> <tabstop>generateIdButton</tabstop> <tabstop>mqttv31Button</tabstop> @@ -382,11 +484,15 @@ <tabstop>usernameEdit</tabstop> <tabstop>passwordEdit</tabstop> <tabstop>willTopicEdit</tabstop> + <tabstop>willMessageEdit</tabstop> <tabstop>willQosSpinBox</tabstop> <tabstop>willRetainCheckBox</tabstop> - <tabstop>willMessageEdit</tabstop> <tabstop>tlsEnableCheckBox</tabstop> <tabstop>tlsCertsFilePicker</tabstop> + <tabstop>connectPropertiesButton</tabstop> + <tabstop>disconnectPropertiesButton</tabstop> + <tabstop>propertiesWidget</tabstop> + <tabstop>samePropertiesCheckBox</tabstop> </tabstops> <resources/> <connections>
--- a/MqttMonitor/MqttConnectionProfilesDialog.py Wed Jul 21 20:10:36 2021 +0200 +++ b/MqttMonitor/MqttConnectionProfilesDialog.py Thu Jul 22 19:02:32 2021 +0200 @@ -8,6 +8,7 @@ """ import collections +import copy from PyQt6.QtCore import pyqtSlot, Qt, QUuid from PyQt6.QtWidgets import ( @@ -39,7 +40,8 @@ "BrokerAddress", "BrokerPort", "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain", - "TlsEnable", "TlsCaCert", "TlsClientCert", "TlsClientKey". + "TlsEnable", "TlsCaCert", "TlsClientCert", "TlsClientKey", + "UserProperties". @type dict @param parent reference to the parent widget @type QWidget @@ -74,6 +76,11 @@ self.profileTabWidget.setCurrentIndex(0) + self.connectPropertiesButton.clicked[bool].connect( + self.__propertiesTypeSelected) + self.disconnectPropertiesButton.clicked[bool].connect( + self.__propertiesTypeSelected) + if len(self.__profiles) == 0: self.minusButton.setEnabled(False) self.copyButton.setEnabled(False) @@ -236,11 +243,11 @@ Public method to return a dictionary of profiles. @return dictionary containing dictionaries containing the defined - connection profiles. Each entry have the keys "BrokerAddress", + connection profiles. Each entry has the keys "BrokerAddress", "BrokerPort", "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", - "TlsClientCert", "TlsClientKey". + "TlsClientCert", "TlsClientKey", "UserProperties". @rtype dict """ profilesDict = {} @@ -263,6 +270,18 @@ else: protocol = MqttProtocols.MQTTv311 + if protocol == MqttProtocols.MQTTv5: + if self.connectPropertiesButton.isChecked(): + self.__userProperties["connect"] = ( + self.propertiesWidget.getProperties()) + else: + self.__userProperties["disconnect"] = ( + self.propertiesWidget.getProperties()) + self.__userProperties["use_connect"] = ( + self.samePropertiesCheckBox.isChecked()) + else: + self.__userProperties = {} + profileName = self.profileEdit.text() connectionProfile = { "BrokerAddress": self.brokerAddressEdit.text(), @@ -282,6 +301,7 @@ "TlsCaCert": "", "TlsClientCert": "", "TlsClientKey": "", + "UserProperties": copy.deepcopy(self.__userProperties), } if connectionProfile["TlsEnable"]: if self.tlsCertsFileButton.isChecked(): @@ -356,23 +376,32 @@ self.brokerAddressEdit.setText(connectionProfile["BrokerAddress"]) self.brokerPortSpinBox.setValue(connectionProfile["BrokerPort"]) self.clientIdEdit.setText(connectionProfile["ClientId"]) + + # general tab self.mqttv31Button.setChecked( connectionProfile["Protocol"] == MqttProtocols.MQTTv31) self.mqttv311Button.setChecked( connectionProfile["Protocol"] == MqttProtocols.MQTTv311) self.mqttv5Button.setChecked( connectionProfile["Protocol"] == MqttProtocols.MQTTv5) + self.on_mqttv5Button_toggled(self.mqttv5Button.isChecked()) self.connectionTimeoutSpinBox.setValue( connectionProfile["ConnectionTimeout"]) self.keepaliveSpinBox.setValue(connectionProfile["Keepalive"]) self.cleanSessionCheckBox.setChecked(connectionProfile["CleanSession"]) + + # user credentials tab self.usernameEdit.setText(connectionProfile["Username"]) self.passwordEdit.setText( pwConvert(connectionProfile["Password"], encode=False)) + + # will tab self.willTopicEdit.setText(connectionProfile["WillTopic"]) self.willMessageEdit.setPlainText(connectionProfile["WillMessage"]) self.willQosSpinBox.setValue(connectionProfile["WillQos"]) self.willRetainCheckBox.setChecked(connectionProfile["WillRetain"]) + + # SSL/TLS tab self.tlsGroupBox.setChecked(connectionProfile["TlsEnable"]) if ( connectionProfile["TlsCaCert"] and @@ -390,11 +419,35 @@ self.tlsCertsFilePicker.setText(connectionProfile["TlsCaCert"]) else: self.tlsDefaultCertsButton.setChecked(True) + + # user properties tab + self.__userProperties = copy.deepcopy( + connectionProfile.get("UserProperties", {})) + if not self.__userProperties: + self.__userProperties = { + "connect": [], + "disconnect": [], + "use_connect": True, + } + + if connectionProfile["Protocol"] == MqttProtocols.MQTTv5: + self.connectPropertiesButton.setChecked(True) + self.propertiesWidget.setProperties( + self.__userProperties["connect"]) + self.samePropertiesCheckBox.setChecked( + self.__userProperties["use_connect"]) + self.disconnectPropertiesButton.setEnabled( + not self.__userProperties["use_connect"]) + else: + self.propertiesWidget.clear() + self.__populatingProfile = False self.showPasswordButton.setChecked(False) self.profileFrame.setEnabled(True) self.__updateApplyButton() + + self.profileTabWidget.setCurrentIndex(0) def __clearProfile(self): """ @@ -422,6 +475,17 @@ self.tlsSelfSignedCertsFilePicker.setText("") self.tlsSelfSignedClientCertFilePicker.setText("") self.tlsSelfSignedClientKeyFilePicker.setText("") + + self.__userProperties = { + "connect": [], + "disconnect": [], + "use_connect": True, + } + self.propertiesWidget.clear() + self.samePropertiesCheckBox.setChecked(True) + self.connectPropertiesButton.setChecked(True) + self.disconnectPropertiesButton.setEnabled(False) + self.__populatingProfile = False self.showPasswordButton.setChecked(False) @@ -508,6 +572,24 @@ self.tlsSelfSignedClientKeyFilePicker.text() != connectionProfile["TlsClientKey"] ) + # check user properties only, if not yet changed + if not changed and protocol == MqttProtocols.MQTTv5: + properties = { + "connect": self.propertiesWidget.getProperties(), + "disconnect": self.__userProperties["disconnect"], + } if self.connectPropertiesButton.isChecked() else { + "connect": self.__userProperties["connect"], + "disconnect": self.propertiesWidget.getProperties(), + } + changed |= ( + self.samePropertiesCheckBox.isChecked() != + connectionProfile["UserProperties"]["use_connect"] or + sorted(properties["connect"]) != + sorted(connectionProfile["UserProperties"]["connect"]) or + sorted(properties["disconnect"]) != + sorted(connectionProfile["UserProperties"]["disconnect"]) + ) + return changed else: @@ -590,6 +672,20 @@ self.__updateApplyButton() @pyqtSlot(bool) + def on_mqttv5Button_toggled(self, checked): + """ + Private slot to handle the selection of the MQTT protocol. + + @param checked state of the button + @type bool + """ + self.profileTabWidget.setTabEnabled( + self.profileTabWidget.indexOf(self.propertiesTab), + checked + ) + # TODO: add code to enable the WILL properties button + + @pyqtSlot(bool) def on_showPasswordButton_toggled(self, checked): """ Private slot to show or hide the password. @@ -715,6 +811,40 @@ """ self.__updateApplyButton() + @pyqtSlot(bool) + def on_samePropertiesCheckBox_toggled(self, checked): + """ + Private slot to handle a change of the properties usage. + + @param checked flag indicating to use the same user properties for + CONNECT and DISCONNECT + @type bool + """ + if checked and not self.connectPropertiesButton.isChecked(): + self.connectPropertiesButton.click() + self.disconnectPropertiesButton.setEnabled(not checked) + + @pyqtSlot(bool) + def __propertiesTypeSelected(self, checked): + """ + Private slot to handle the switching of the user properties type. + + @param checked state of the buttons + @type bool + """ + if checked: + # handle the selection only + if self.connectPropertiesButton.isChecked(): + self.__userProperties["disconnect"] = ( + self.propertiesWidget.getProperties()) + self.propertiesWidget.setProperties( + self.__userProperties["connect"]) + else: + self.__userProperties["connect"] = ( + self.propertiesWidget.getProperties()) + self.propertiesWidget.setProperties( + self.__userProperties["disconnect"]) + @pyqtSlot() def reject(self): """
--- a/MqttMonitor/MqttConnectionProfilesDialog.ui Wed Jul 21 20:10:36 2021 +0200 +++ b/MqttMonitor/MqttConnectionProfilesDialog.ui Thu Jul 22 19:02:32 2021 +0200 @@ -740,6 +740,63 @@ </item> </layout> </widget> + <widget class="QWidget" name="propertiesTab"> + <attribute name="title"> + <string>User Properties</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_8"> + <item> + <widget class="QRadioButton" name="connectPropertiesButton"> + <property name="toolTip"> + <string>Select to edit the CONNECT user properties</string> + </property> + <property name="text"> + <string>CONNECT</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="disconnectPropertiesButton"> + <property name="toolTip"> + <string>Select to edit the DISCONNECT user properties</string> + </property> + <property name="text"> + <string>DISCONNECT</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="MqttUserPropertiesEditor" name="propertiesWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::StrongFocus</enum> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="samePropertiesCheckBox"> + <property name="toolTip"> + <string>Select to use the CONNECT user properties when disconnecting</string> + </property> + <property name="text"> + <string>Use CONNECT properties for DISCONNECT</string> + </property> + </widget> + </item> + </layout> + </widget> </widget> </item> <item> @@ -772,6 +829,12 @@ <header>EricWidgets/EricPathPicker.h</header> <container>1</container> </customwidget> + <customwidget> + <class>MqttUserPropertiesEditor</class> + <extends>QWidget</extends> + <header>MqttMonitor/MqttUserPropertiesEditor.h</header> + <container>1</container> + </customwidget> </customwidgets> <tabstops> <tabstop>profilesList</tabstop> @@ -794,9 +857,9 @@ <tabstop>passwordEdit</tabstop> <tabstop>showPasswordButton</tabstop> <tabstop>willTopicEdit</tabstop> + <tabstop>willMessageEdit</tabstop> <tabstop>willQosSpinBox</tabstop> <tabstop>willRetainCheckBox</tabstop> - <tabstop>willMessageEdit</tabstop> <tabstop>tlsGroupBox</tabstop> <tabstop>tlsDefaultCertsButton</tabstop> <tabstop>tlsCertsFileButton</tabstop> @@ -805,6 +868,10 @@ <tabstop>tlsSelfSignedCertsFilePicker</tabstop> <tabstop>tlsSelfSignedClientCertFilePicker</tabstop> <tabstop>tlsSelfSignedClientKeyFilePicker</tabstop> + <tabstop>connectPropertiesButton</tabstop> + <tabstop>disconnectPropertiesButton</tabstop> + <tabstop>propertiesWidget</tabstop> + <tabstop>samePropertiesCheckBox</tabstop> </tabstops> <resources/> <connections>
--- a/MqttMonitor/MqttMonitorWidget.py Wed Jul 21 20:10:36 2021 +0200 +++ b/MqttMonitor/MqttMonitorWidget.py Thu Jul 22 19:02:32 2021 +0200 @@ -74,6 +74,8 @@ self.__messagesTopicFormat.setFontWeight(QFont.Weight.Bold) self.__messagesQosFormat = self.messagesEdit.currentCharFormat() self.__messagesQosFormat.setFontItalic(True) + self.__messagesSubheaderFormat = self.messagesEdit.currentCharFormat() + self.__messagesSubheaderFormat.setFontUnderline(True) self.__propertiesFormat = self.propertiesEdit.currentCharFormat() self.__propertiesTopicFormat = self.propertiesEdit.currentCharFormat() @@ -159,6 +161,10 @@ self.__publishedTopics = [] self.__updatePublishTopicComboBox() self.publishButton.setEnabled(False) + self.publishPropertiesButton.setIcon( + UI.PixmapCache.getIcon("listSelection")) + self.publishPropertiesButton.setEnabled(False) + self.publishPropertiesButton.setVisible(False) self.__connectionOptions = None @@ -319,6 +325,8 @@ self.__client.getProtocol() == MqttProtocols.MQTTv5) self.publishGroup.setEnabled(True) self.brokerStatusButton.setEnabled(True) + self.publishPropertiesButton.setVisible( + self.__client.getProtocol() == MqttProtocols.MQTTv5) self.__statusLoadValues.clear() self.__clearBrokerStatusLabels() @@ -376,6 +384,7 @@ self.unsubscribeGroup.setEnabled(False) self.unsubscribePropertiesButton.setVisible(False) self.publishGroup.setEnabled(False) + self.publishPropertiesButton.setVisible(False) self.brokerStatusButton.setEnabled(False) self.__statusLoadValues.clear() @@ -466,6 +475,8 @@ @param mid ID of the subscribe request @type int """ + # TODO: remember the successfully subscribed topic + # TODO: max. number of recent topics as a config item if mid in self.__topicQueue: topic = self.__topicQueue.pop(mid) self.__subscribedTopics.append(topic) @@ -723,6 +734,18 @@ topic, properties=properties) self.__topicQueue[mid] = topic + @pyqtSlot() + def on_publishPropertiesButton_clicked(self): + """ + Private slot to edit the publish user properties. + """ + topic = self.publishTopicComboBox.currentText() + self.__editProperties( + "publish", + self.tr("PUBLISH: User Properties for '{0}'").format(topic), + topic + ) + @pyqtSlot(str) def on_publishTopicComboBox_editTextChanged(self, topic): """ @@ -732,6 +755,7 @@ @type str """ self.publishButton.setEnabled(bool(topic)) + self.publishPropertiesButton.setEnabled(bool(topic)) @pyqtSlot() def on_publishButton_clicked(self): @@ -765,8 +789,15 @@ # use empty string together with the retain flag to clean # a retained message by sending None instead payloadStr = None + properties = ( + self.__plugin.getPreferences("PublishProperties").get(topic, []) + if self.__client.getProtocol() == MqttProtocols.MQTTv5 else + None + ) - msgInfo = self.__client.publish(topic, payloadStr, qos, retain) + msgInfo = self.__client.publish( + topic, payload=payloadStr, qos=qos, retain=retain, + properties=properties) if msgInfo.rc == 0: if topic not in self.__publishedTopics: self.__publishedTopics.append(topic) @@ -780,8 +811,14 @@ Private slot to clear the retained messages for the topic. """ topic = self.publishTopicComboBox.currentText() + properties = ( + self.__plugin.getPreferences("PublishProperties").get(topic, []) + if self.__client.getProtocol() == MqttProtocols.MQTTv5 else + None + ) - msgInfo = self.__client.publish(topic, payload=None, retain=True) + msgInfo = self.__client.publish( + topic, payload=None, retain=True, properties=properties) if msgInfo.rc == 0: if topic not in self.__publishedTopics: self.__publishedTopics.append(topic) @@ -988,6 +1025,7 @@ brokerList.remove(hostAndPort) brokerList.insert(0, hostAndPort) # limit to most recently used 20 entries + # TODO: make the amount of recent brokers a config item brokerList = brokerList[:20] self.__plugin.setPreferences("RecentBrokersWithPort", brokerList) @@ -1112,19 +1150,25 @@ self.messagesEdit.insertPlainText(self.tr("Retained Message\n")) if properties: - self.messagesEdit.setCurrentCharFormat(self.__messagesTopicFormat) + self.messagesEdit.setCurrentCharFormat( + self.__messagesSubheaderFormat) self.messagesEdit.insertPlainText(self.tr("Properties:\n")) self.messagesEdit.setCurrentCharFormat(self.__messagesFormat) - for name, value in sorted(properties.items): + for name, value in sorted(properties.items()): self.messagesEdit.insertPlainText( self.tr("{0}: {1}\n", "property name, property value") .format(name, value) ) + self.messagesEdit.setCurrentCharFormat(self.__messagesSubheaderFormat) + self.messagesEdit.insertPlainText(self.tr("Message:\n")) payloadStr = str(payload, encoding="utf-8", errors="replace") + payloadStr = Utilities.filterAnsiSequences(payloadStr) self.messagesEdit.setCurrentCharFormat(self.__messagesFormat) - self.messagesEdit.insertPlainText( - Utilities.filterAnsiSequences(payloadStr)) + if payloadStr: + self.messagesEdit.insertPlainText(payloadStr) + else: + self.messagesEdit.insertPlainText(self.tr("<empty>")) if self.followMessagesCheckBox.isChecked(): self.messagesEdit.ensureCursorVisible() @@ -1273,7 +1317,7 @@ self.__plugin.setPreferences("MostRecentProfile", profileName) profilesDict = self.__plugin.getPreferences("BrokerProfiles") - connectionProfile = copy.copy(profilesDict[profileName]) + connectionProfile = copy.deepcopy(profilesDict[profileName]) host = connectionProfile["BrokerAddress"] port = connectionProfile["BrokerPort"] try: @@ -1348,11 +1392,12 @@ @param key key to retrieve the right properties @type str """ - from .MqttUserPropertiesEditor import MqttUserPropertiesEditor + from .MqttUserPropertiesEditor import MqttUserPropertiesEditorDialog preferencesKey = "{0}Properties".format(propertiesType.capitalize()) properties = self.__plugin.getPreferences(preferencesKey) - dlg = MqttUserPropertiesEditor(header, properties.get(key, []), self) + dlg = MqttUserPropertiesEditorDialog( + header, properties.get(key, []), self) if dlg.exec() == QDialog.DialogCode.Accepted: properties[key] = dlg.getProperties() self.__plugin.setPreferences(preferencesKey, properties)
--- a/MqttMonitor/MqttMonitorWidget.ui Wed Jul 21 20:10:36 2021 +0200 +++ b/MqttMonitor/MqttMonitorWidget.ui Thu Jul 22 19:02:32 2021 +0200 @@ -351,6 +351,16 @@ </property> </widget> </item> + <item> + <widget class="QToolButton" name="publishPropertiesButton"> + <property name="toolTip"> + <string>Press to edit the user properties</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> </layout> </item> <item> @@ -614,8 +624,8 @@ <rect> <x>0</x> <y>0</y> - <width>344</width> - <height>840</height> + <width>339</width> + <height>670</height> </rect> </property> <layout class="QFormLayout" name="formLayout"> @@ -1471,13 +1481,16 @@ <tabstop>subscribeTopicEdit</tabstop> <tabstop>subscribeQosSpinBox</tabstop> <tabstop>subscribeButton</tabstop> + <tabstop>subscribePropertiesButton</tabstop> <tabstop>unsubscribeTopicComboBox</tabstop> <tabstop>unsubscribeButton</tabstop> + <tabstop>unsubscribePropertiesButton</tabstop> <tabstop>publishTopicComboBox</tabstop> <tabstop>publishPayloadEdit</tabstop> <tabstop>publishPayloadFilePicker</tabstop> <tabstop>publishQosSpinBox</tabstop> <tabstop>publishRetainCheckBox</tabstop> + <tabstop>publishPropertiesButton</tabstop> <tabstop>publishButton</tabstop> <tabstop>publishClearButton</tabstop> <tabstop>publishClearRetainedButton</tabstop>
--- a/MqttMonitor/MqttUserPropertiesEditor.py Wed Jul 21 20:10:36 2021 +0200 +++ b/MqttMonitor/MqttUserPropertiesEditor.py Thu Jul 22 19:02:32 2021 +0200 @@ -7,26 +7,24 @@ Module implementing an editor for MQTT v5 user properties. """ -from PyQt6.QtCore import pyqtSlot -from PyQt6.QtWidgets import QDialog, QTableWidgetItem +from PyQt6.QtCore import pyqtSlot, Qt +from PyQt6.QtWidgets import ( + QDialog, QDialogButtonBox, QTableWidgetItem, QVBoxLayout, QLabel, QWidget +) from .Ui_MqttUserPropertiesEditor import Ui_MqttUserPropertiesEditor import UI.PixmapCache -class MqttUserPropertiesEditor(QDialog, Ui_MqttUserPropertiesEditor): +class MqttUserPropertiesEditor(QWidget, Ui_MqttUserPropertiesEditor): """ Class implementing an editor for MQTT v5 user properties. """ - def __init__(self, header, properties, parent=None): + def __init__(self, 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) """ @@ -37,13 +35,7 @@ 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.clearButton.clicked.connect(self.clear) self.deleteButton.setEnabled(False) @@ -76,14 +68,29 @@ self.propertiesTable.removeRow(row) @pyqtSlot() - def on_clearButton_clicked(self): + def clear(self): """ - Private slot to delete all properties. + Public slot to delete all properties. """ self.propertiesTable.clearContents() self.propertiesTable.setRowCount(10) self.propertiesTable.setCurrentCell(0, 0) + def setProperties(self, properties): + """ + Public method to populate the editor with a list of user properties. + + @param properties list of defined user properties + @type list of tuple of (str, str) + """ + 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)) + else: + self.clear() + def getProperties(self): """ Public method to get the list of defined user properties. @@ -99,6 +106,59 @@ if key: valueItem = self.propertiesTable.item(row, 1) value = valueItem.text() if valueItem else "" - properties.append((key, value)) + properties.append([key, value]) return properties + + +class MqttUserPropertiesEditorDialog(QDialog): + """ + Class implementing an editor dialog 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.setObjectName("MqttUserPropertiesEditor") + self.resize(400, 300) + self.setSizeGripEnabled(True) + self.setWindowTitle(self.tr("User Properties")) + + self.__layout = QVBoxLayout(self) + + self.__headerLabel = QLabel(header, self) + self.__layout.addWidget(self.__headerLabel) + + self.__propertiesEditor = MqttUserPropertiesEditor(self) + self.__layout.addWidget(self.__propertiesEditor) + + self.__buttonBox = QDialogButtonBox(self) + self.__buttonBox.setOrientation(Qt.Orientation.Horizontal) + self.__buttonBox.setStandardButtons( + QDialogButtonBox.StandardButton.Cancel | + QDialogButtonBox.StandardButton.Ok) + self.__buttonBox.setObjectName("buttonBox") + self.__layout.addWidget(self.__buttonBox) + + self.__buttonBox.accepted.connect(self.accept) + self.__buttonBox.rejected.connect(self.reject) + + self.__propertiesEditor.setProperties(properties) + + 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) + """ + return self.__propertiesEditor.getProperties()
--- a/MqttMonitor/MqttUserPropertiesEditor.ui Wed Jul 21 20:10:36 2021 +0200 +++ b/MqttMonitor/MqttUserPropertiesEditor.ui Thu Jul 22 19:02:32 2021 +0200 @@ -1,33 +1,35 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>MqttUserPropertiesEditor</class> - <widget class="QDialog" name="MqttUserPropertiesEditor"> + <widget class="QWidget" name="MqttUserPropertiesEditor"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>400</width> - <height>350</height> + <height>250</height> </rect> </property> <property name="windowTitle"> <string>User Properties</string> </property> - <property name="sizeGripEnabled"> + <property name="sizeGripEnabled" stdset="0"> <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"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> <widget class="QTableWidget" name="propertiesTable"> <property name="alternatingRowColors"> <bool>true</bool> @@ -66,7 +68,7 @@ </column> </widget> </item> - <item row="1" column="1"> + <item> <layout class="QVBoxLayout" name="verticalLayout"> <item> <widget class="QToolButton" name="addButton"> @@ -113,51 +115,8 @@ </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> + <connections/> </ui>
--- a/PluginMqttMonitor.py Wed Jul 21 20:10:36 2021 +0200 +++ b/PluginMqttMonitor.py Thu Jul 22 19:02:32 2021 +0200 @@ -24,7 +24,7 @@ author = "Detlev Offenbach <detlev@die-offenbachs.de>" autoactivate = True deactivateable = True -version = "1.0.1" +version = "1.1.0" className = "MqttMonitorPlugin" packageName = "MqttMonitor" shortDescription = "Plug-in implementing a tool to connect to a MQTT broker" @@ -95,9 +95,11 @@ "BrokerProfiles": "{}", # JSON formatted empty dict # __IGNORE_WARNING_M613__ "MostRecentProfile": "", # most recently used profile + "PublishProperties": "{}", # JSON formatted empty dict + # __IGNORE_WARNING_M613__ "SubscribeProperties": "{}", # JSON formatted empty dict # __IGNORE_WARNING_M613__ - "UnsubscribeProperties": "{}", # JSON formatted empty dict + "UnsubscribeProperties": "{}", # JSON formatted empty dict # __IGNORE_WARNING_M613__ } @@ -217,7 +219,8 @@ @rtype Any """ if key in ["RecentBrokersWithPort", "BrokerProfiles", - "SubscribeProperties", "UnsubscribeProperties"]: + "SubscribeProperties", "UnsubscribeProperties", + "PublishProperties"]: return json.loads(Preferences.Prefs.settings.value( self.PreferencesKey + "/" + key, self.__defaults[key])) else: @@ -234,7 +237,8 @@ @type Any """ if key in ["RecentBrokersWithPort", "BrokerProfiles", - "SubscribeProperties", "UnsubscribeProperties"]: + "SubscribeProperties", "UnsubscribeProperties", + "PublishProperties"]: Preferences.Prefs.settings.setValue( self.PreferencesKey + "/" + key, json.dumps(value)) else: