Fri, 23 Jul 2021 17:48:22 +0200
Added support for 'Last Will' user properties and a button to clear the last will.
--- a/MqttMonitor/MqttClient.py Thu Jul 22 19:02:32 2021 +0200 +++ b/MqttMonitor/MqttClient.py Fri Jul 23 17:48:22 2021 +0200 @@ -279,8 +279,8 @@ """ self.__mqttClient.user_data_set(userdata) - # TODO: MQTTv5: add support for WILL properties - def setLastWill(self, topic, payload=None, qos=0, retain=False): + def setLastWill(self, topic, payload=None, qos=0, retain=False, + properties=None): """ Public method to set the last will of the client. @@ -293,9 +293,12 @@ @param retain flag indicating to set as the "last known good"/retained message for the will topic @type bool + @param properties list of user properties to be sent with the + last will message + @type list of tuple of (str, str) """ self.__mqttClient.will_set(topic, payload=payload, qos=qos, - retain=retain) + retain=retain, properties=properties) def clearLastWill(self): """ @@ -351,7 +354,7 @@ self.__loopStarted = False def connectToServer(self, host, port=1883, keepalive=60, bindAddress="", - properties=None): + properties=None, clearWill=False): """ Public method to connect to a remote MQTT broker. @@ -369,7 +372,12 @@ @param properties list of user properties to be sent with the subscription @type list of tuple of (str, str) + @param clearWill flag indicating to clear the last will previously set + @type bool """ + if clearWill: + self.clearLastWill() + props = ( self.__createPropertiesObject(PacketTypes.CONNECT, properties) if properties else @@ -385,7 +393,7 @@ self.startLoop() def connectToServerWithOptions(self, host, port=1883, bindAddress="", - options=None): + options=None, clearWill=False): """ Public method to connect to a remote MQTT broker. @@ -400,9 +408,12 @@ @param options dictionary containing the connection options. This dictionary should contain the keys "ClientId", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", - "WillMessage", "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", - "TlsClientCert", "TlsClientKey", "UserProperties". + "WillMessage", "WillQos", "WillRetain", "WillProperties", + "TlsEnable", "TlsCaCert", "TlsClientCert", "TlsClientKey", + "UserProperties". @type dict + @param clearWill flag indicating to clear the last will previously set + @type bool """ if options: parametersDict = self.defaultConnectionOptions() @@ -420,16 +431,25 @@ self.setUserCredentials(parametersDict["Username"]) # step 2: set last will data - if parametersDict["WillTopic"]: + if not clearWill and parametersDict["WillTopic"]: if parametersDict["WillMessage"]: willMessage = parametersDict["WillMessage"] else: # empty message to clear the will willMessage = None + props = ( + self.__createPropertiesObject( + PacketTypes.WILLMESSAGE, + parametersDict["WillProperties"]) + if (parametersDict["WillProperties"] and + self.__protocol == MqttProtocols.MQTTv5) else + None + ) self.setLastWill(parametersDict["WillTopic"], - willMessage, - parametersDict["WillQos"], - parametersDict["WillRetain"]) + payload=willMessage, + qos=parametersDict["WillQos"], + retain=parametersDict["WillRetain"], + properties=props) # step 3: set TLS parameters if parametersDict["TlsEnable"]: @@ -466,11 +486,13 @@ self.__cleanSession = parametersDict["CleanSession"] self.connectToServer(host, port=port, keepalive=parametersDict["Keepalive"], - properties=properties) + properties=properties, + clearWill=clearWill) else: keepalive = self.defaultConnectionOptions["Keepalive"] self.connectToServer(host, port=port, keepalive=keepalive, - bindAddress=bindAddress) + bindAddress=bindAddress, + clearWill=clearWill) @classmethod def defaultConnectionOptions(cls): @@ -481,8 +503,8 @@ @return dictionary containing the default connection options. It has the keys "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", - "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", "TlsClientCert", - "TlsClientKey", "UserProperties". + "WillQos", "WillRetain", "WillProperties", "TlsEnable", + "TlsCaCert", "TlsClientCert", "TlsClientKey", "UserProperties". @rtype dict """ return { @@ -497,6 +519,7 @@ "WillMessage": "", "WillQos": 0, "WillRetain": False, + "WillProperties": [], "TlsEnable": False, "TlsCaCert": "", "TlsClientCert": "",
--- a/MqttMonitor/MqttConnectionOptionsDialog.py Thu Jul 22 19:02:32 2021 +0200 +++ b/MqttMonitor/MqttConnectionOptionsDialog.py Fri Jul 23 17:48:22 2021 +0200 @@ -20,13 +20,13 @@ from .MqttClient import MqttClient, MqttProtocols from Utilities.crypto import pwConvert +import UI.PixmapCache class MqttConnectionOptionsDialog(QDialog, Ui_MqttConnectionOptionsDialog): """ Class implementing a dialog to enter MQTT connection options. """ - # TODO: add WILL user properties def __init__(self, options=None, parent=None): """ Constructor @@ -35,7 +35,8 @@ populate the dialog with. It must have the keys "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", "WillQos", - "WillRetain", "TlsEnable", "TlsCaCert", "UserProperties". + "WillRetain", "WillProperties", "TlsEnable", "TlsCaCert", + "UserProperties". @type dict @param parent reference to the parent widget @type QWidget @@ -47,6 +48,11 @@ self.tlsCertsFilePicker.setFilters( self.tr("Certificate Files (*.crt *.pem);;All Files (*)")) + self.willPropertiesButton.setIcon( + UI.PixmapCache.getIcon("listSelection")) + + self.optionsWidget.setCurrentIndex(0) + # initialize MQTTv5 related stuff self.on_mqttv5Button_toggled(False) @@ -119,7 +125,8 @@ self.optionsWidget.indexOf(self.propertiesTab), checked ) - # TODO: add code to enable the WILL properties button + self.willPropertiesButton.setEnabled(checked) + self.willPropertiesButton.setVisible(checked) @pyqtSlot(QAbstractButton) def on_buttonBox_clicked(self, button): @@ -168,6 +175,18 @@ self.propertiesWidget.setProperties( self.__userProperties["disconnect"]) + @pyqtSlot() + def on_willPropertiesButton_clicked(self): + """ + Private slot to edit the last will user properties. + """ + from .MqttUserPropertiesEditor import MqttUserPropertiesEditorDialog + + dlg = MqttUserPropertiesEditorDialog( + self.tr("Last Will User Properties"), self.__willProperties, self) + if dlg.exec() == QDialog.DialogCode.Accepted: + self.__willProperties = dlg.getProperties() + def __populateDefaults(self, options=None): """ Private method to populate the dialog. @@ -179,7 +198,7 @@ the dialog with. It must have the keys "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain", - "TlsEnable", "TlsCaCert", "UserProperties". + "WillProperties", "TlsEnable", "TlsCaCert", "UserProperties". @type dict """ if options is None: @@ -206,6 +225,8 @@ self.willRetainCheckBox.setChecked(options["WillRetain"]) self.willTopicEdit.setText(options["WillTopic"]) self.willMessageEdit.setPlainText(options["WillMessage"]) + self.__willProperties = copy.deepcopy( + options.get("WillProperties", [])) # TLS parameters self.tlsEnableCheckBox.setChecked(options["TlsEnable"]) @@ -239,8 +260,8 @@ @return dictionary containing the connection options. It has the keys "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", - "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", - "UserProperties". + "WillQos", "WillRetain", "WillProperties", "TlsEnable", + "TlsCaCert", "UserProperties". @rtype dict """ if self.mqttv31Button.isChecked(): @@ -263,6 +284,7 @@ self.samePropertiesCheckBox.isChecked()) else: self.__userProperties = {} + self.__willProperties = [] return { "ClientId": self.clientIdEdit.text(), @@ -276,6 +298,7 @@ "WillMessage": self.willMessageEdit.toPlainText(), "WillQos": self.willQosSpinBox.value(), "WillRetain": self.willRetainCheckBox.isChecked(), + "WillProperties": copy.deepcopy(self.__willProperties), "TlsEnable": self.tlsEnableCheckBox.isChecked(), "TlsCaCert": self.tlsCertsFilePicker.text(), "UserProperties": copy.deepcopy(self.__userProperties),
--- a/MqttMonitor/MqttConnectionOptionsDialog.ui Thu Jul 22 19:02:32 2021 +0200 +++ b/MqttMonitor/MqttConnectionOptionsDialog.ui Fri Jul 23 17:48:22 2021 +0200 @@ -275,6 +275,16 @@ <string>Last Will</string> </attribute> <layout class="QGridLayout" name="gridLayout_2"> + <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"> @@ -282,19 +292,6 @@ </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"> @@ -318,13 +315,26 @@ </property> </widget> </item> - <item row="0" column="0"> - <widget class="QLineEdit" name="willTopicEdit"> + <item row="0" column="4"> + <widget class="QToolButton" name="willPropertiesButton"> <property name="toolTip"> - <string>Enter the topic of the last will</string> + <string>Press to edit the user properties</string> + </property> + <property name="text"> + <string/> </property> - <property name="clearButtonEnabled"> - <bool>true</bool> + </widget> + </item> + <item row="1" column="0" colspan="5"> + <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> @@ -487,6 +497,7 @@ <tabstop>willMessageEdit</tabstop> <tabstop>willQosSpinBox</tabstop> <tabstop>willRetainCheckBox</tabstop> + <tabstop>willPropertiesButton</tabstop> <tabstop>tlsEnableCheckBox</tabstop> <tabstop>tlsCertsFilePicker</tabstop> <tabstop>connectPropertiesButton</tabstop>
--- a/MqttMonitor/MqttConnectionProfilesDialog.py Thu Jul 22 19:02:32 2021 +0200 +++ b/MqttMonitor/MqttConnectionProfilesDialog.py Fri Jul 23 17:48:22 2021 +0200 @@ -31,7 +31,7 @@ """ Class implementing a dialog to edit the MQTT connection profiles. """ - def __init__(self, profiles, parent=None): + def __init__(self, profiles, currentProfile="", parent=None): """ Constructor @@ -40,9 +40,11 @@ "BrokerAddress", "BrokerPort", "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain", - "TlsEnable", "TlsCaCert", "TlsClientCert", "TlsClientKey", - "UserProperties". + "WillProperties", "TlsEnable", "TlsCaCert", "TlsClientCert", + "TlsClientKey", "UserProperties". @type dict + @param currentProfile name of the currently selected profile + @type str @param parent reference to the parent widget @type QWidget """ @@ -50,13 +52,15 @@ self.setupUi(self) self.__profiles = collections.defaultdict(self.__defaultProfile) - self.__profiles.update(profiles) + self.__profiles.update(copy.deepcopy(profiles)) self.__profilesChanged = False self.plusButton.setIcon(UI.PixmapCache.getIcon("plus")) self.copyButton.setIcon(UI.PixmapCache.getIcon("editCopy")) self.minusButton.setIcon(UI.PixmapCache.getIcon("minus")) self.showPasswordButton.setIcon(UI.PixmapCache.getIcon("showPassword")) + self.willPropertiesButton.setIcon( + UI.PixmapCache.getIcon("listSelection")) self.tlsCertsFilePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE) self.tlsCertsFilePicker.setFilters( @@ -89,7 +93,7 @@ self.__populatingProfile = False self.__deletingProfile = False - self.__populateProfilesList() + self.__populateProfilesList(currentProfile=currentProfile) @pyqtSlot(str) def on_profileEdit_textChanged(self, name): @@ -113,7 +117,7 @@ QDialogButtonBox.StandardButton.Apply ): currentProfile = self.__applyProfile() - self.__populateProfilesList(currentProfile) + self.__populateProfilesList(currentProfile=currentProfile) elif button == self.profileButtonBox.button( QDialogButtonBox.StandardButton.Reset @@ -206,9 +210,11 @@ """ already. Aborting...</p>""").format( newProfileName)) else: - profile = self.__defaultProfile() - profile.update(self.__profiles[profileName]) - self.__profiles[newProfileName] = profile + connectionProfile = self.__defaultProfile() + connectionProfile.update( + copy.deepcopy(self.__profiles[profileName])) + self.__profiles[newProfileName] = connectionProfile + self.__profilesChanged = True itm = QListWidgetItem(newProfileName, self.profilesList) self.profilesList.setCurrentItem(itm) @@ -246,12 +252,13 @@ connection profiles. Each entry has the keys "BrokerAddress", "BrokerPort", "ClientId", "Protocol", "ConnectionTimeout", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", - "WillMessage", "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", - "TlsClientCert", "TlsClientKey", "UserProperties". + "WillMessage", "WillQos", "WillRetain", "WillProperties", + "TlsEnable", "TlsCaCert", "TlsClientCert", "TlsClientKey", + "UserProperties". @rtype dict """ profilesDict = {} - profilesDict.update(self.__profiles) + profilesDict.update(copy.deepcopy(dict(self.__profiles))) return profilesDict def __applyProfile(self): @@ -281,6 +288,7 @@ self.samePropertiesCheckBox.isChecked()) else: self.__userProperties = {} + self.__willProperties = [] profileName = self.profileEdit.text() connectionProfile = { @@ -297,6 +305,7 @@ "WillMessage": self.willMessageEdit.toPlainText(), "WillQos": self.willQosSpinBox.value(), "WillRetain": self.willRetainCheckBox.isChecked(), + "WillProperties": copy.deepcopy(self.__willProperties), "TlsEnable": self.tlsGroupBox.isChecked(), "TlsCaCert": "", "TlsClientCert": "", @@ -400,6 +409,8 @@ self.willMessageEdit.setPlainText(connectionProfile["WillMessage"]) self.willQosSpinBox.setValue(connectionProfile["WillQos"]) self.willRetainCheckBox.setChecked(connectionProfile["WillRetain"]) + self.__willProperties = copy.deepcopy( + connectionProfile.get("WillProperties", [])) # SSL/TLS tab self.tlsGroupBox.setChecked(connectionProfile["TlsEnable"]) @@ -467,6 +478,7 @@ self.willMessageEdit.setPlainText("") self.willQosSpinBox.setValue(0) self.willRetainCheckBox.setChecked(False) + self.__willProperties = [] self.tlsGroupBox.setChecked(False) self.tlsDefaultCertsButton.setChecked(True) self.tlsCertsFileButton.setChecked(True) @@ -556,6 +568,12 @@ connectionProfile["WillRetain"] or self.tlsGroupBox.isChecked() != connectionProfile["TlsEnable"] ) + # check will properties only, ig not yet changed + if not changed and protocol == MqttProtocols.MQTTv5: + changed |= ( + sorted(self.__willProperties) != + sorted(connectionProfile["WillProperties"]) + ) # check TLS stuff only, if not yet changed if not changed: if self.tlsCertsFileButton.isChecked(): @@ -683,7 +701,8 @@ self.profileTabWidget.indexOf(self.propertiesTab), checked ) - # TODO: add code to enable the WILL properties button + self.willPropertiesButton.setEnabled(checked) + self.willPropertiesButton.setVisible(checked) @pyqtSlot(bool) def on_showPasswordButton_toggled(self, checked): @@ -699,6 +718,18 @@ self.passwordEdit.setEchoMode(QLineEdit.EchoMode.Password) ) + @pyqtSlot() + def on_willPropertiesButton_clicked(self): + """ + Private slot to edit the last will user properties. + """ + from .MqttUserPropertiesEditor import MqttUserPropertiesEditorDialog + + dlg = MqttUserPropertiesEditorDialog( + self.tr("Last Will User Properties"), self.__willProperties, self) + if dlg.exec() == QDialog.DialogCode.Accepted: + self.__willProperties = dlg.getProperties() + @pyqtSlot(str) def on_tlsCertsFilePicker_textChanged(self, path): """
--- a/MqttMonitor/MqttConnectionProfilesDialog.ui Thu Jul 22 19:02:32 2021 +0200 +++ b/MqttMonitor/MqttConnectionProfilesDialog.ui Fri Jul 23 17:48:22 2021 +0200 @@ -466,6 +466,13 @@ </widget> </item> <item row="0" column="1"> + <widget class="QLabel" name="label_13"> + <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> @@ -478,7 +485,7 @@ </property> </widget> </item> - <item row="0" column="2"> + <item row="0" column="3"> <widget class="QCheckBox" name="willRetainCheckBox"> <property name="toolTip"> <string>Select to retain the last will message</string> @@ -488,7 +495,17 @@ </property> </widget> </item> - <item row="1" column="0" colspan="3"> + <item row="0" column="4"> + <widget class="QToolButton" name="willPropertiesButton"> + <property name="toolTip"> + <string>Press to edit the user properties</string> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="0" colspan="5"> <widget class="QPlainTextEdit" name="willMessageEdit"> <property name="maximumSize"> <size> @@ -860,6 +877,7 @@ <tabstop>willMessageEdit</tabstop> <tabstop>willQosSpinBox</tabstop> <tabstop>willRetainCheckBox</tabstop> + <tabstop>willPropertiesButton</tabstop> <tabstop>tlsGroupBox</tabstop> <tabstop>tlsDefaultCertsButton</tabstop> <tabstop>tlsCertsFileButton</tabstop>
--- a/MqttMonitor/MqttMonitorWidget.py Thu Jul 22 19:02:32 2021 +0200 +++ b/MqttMonitor/MqttMonitorWidget.py Fri Jul 23 17:48:22 2021 +0200 @@ -137,6 +137,8 @@ )) self.__populateBrokerComboBoxes() self.brokerStatusLabel.hide() + self.clearWillButton.setIcon( + UI.PixmapCache.getIcon("certificateDelete")) self.subscribeButton.setIcon(UI.PixmapCache.getIcon("plus")) self.subscribeButton.setEnabled(False) @@ -294,16 +296,22 @@ """ self.brokerStatusLabel.hide() - # TODO: add support for flags[‘session present’] if rc == 0: self.__connectedToBroker = True self.__connectionOptions = None + try: + sessionPresent = flags["session present"] == 1 + except KeyError: + sessionPresent = False + msg = ( mqttReasonCode(rc, packetType) if packetType is not None else mqttConnackMessage(rc) ) + if sessionPresent: + msg = self.tr("{0} - Session still present").format(msg) self.__flashBrokerStatusLabel(msg) if properties: @@ -600,8 +608,10 @@ from .MqttConnectionProfilesDialog import ( MqttConnectionProfilesDialog ) + profileName = self.profileComboBox.currentText() dlg = MqttConnectionProfilesDialog( - self.__plugin.getPreferences("BrokerProfiles"), parent=self) + self.__plugin.getPreferences("BrokerProfiles"), + currentProfile=profileName, parent=self) if dlg.exec() == QDialog.DialogCode.Accepted: profilesDict = dlg.getProfiles() self.__plugin.setPreferences("BrokerProfiles", profilesDict) @@ -1296,9 +1306,17 @@ self.__addBrokerToRecent(host, port) self.connectButton.setEnabled(False) + + if self.clearWillButton.isChecked(): + clearWill = True + self.clearWillButton.setChecked(False) + else: + clearWill = False + if self.__connectionOptions is None: self.__client = self.__createClient() - self.__client.connectToServer(host, port=port) + self.__client.connectToServer( + host, port=port, clearWill=clearWill) else: self.__client = self.__createClient( clientId=self.__connectionOptions["ClientId"], @@ -1306,7 +1324,8 @@ protocol=self.__connectionOptions["Protocol"] ) self.__client.connectToServerWithOptions( - host, port=port, options=self.__connectionOptions) + host, port=port, options=self.__connectionOptions, + clearWill=clearWill) def __profileConnectToBroker(self): """ @@ -1332,13 +1351,20 @@ self.connectButton.setEnabled(False) + if self.clearWillButton.isChecked(): + clearWill = True + self.clearWillButton.setChecked(False) + else: + clearWill = False + self.__client = self.__createClient( clientId=connectionProfile["ClientId"], cleanSession=connectionProfile["CleanSession"], protocol=protocol ) self.__client.connectToServerWithOptions( - host, port=port, options=connectionProfile) + host, port=port, options=connectionProfile, + clearWill=clearWill) def __showProperties(self, typeStr, properties): """
--- a/MqttMonitor/MqttMonitorWidget.ui Thu Jul 22 19:02:32 2021 +0200 +++ b/MqttMonitor/MqttMonitorWidget.ui Fri Jul 23 17:48:22 2021 +0200 @@ -136,6 +136,19 @@ </property> </widget> </item> + <item> + <widget class="QToolButton" name="clearWillButton"> + <property name="toolTip"> + <string>Select to clear a previously set last will message</string> + </property> + <property name="text"> + <string/> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> </layout> </item> <item row="1" column="0" colspan="3"> @@ -1477,6 +1490,7 @@ <tabstop>brokerPortComboBox</tabstop> <tabstop>brokerConnectionOptionsButton</tabstop> <tabstop>connectButton</tabstop> + <tabstop>clearWillButton</tabstop> <tabstop>brokerWidget</tabstop> <tabstop>subscribeTopicEdit</tabstop> <tabstop>subscribeQosSpinBox</tabstop>