Sat, 08 Sep 2018 16:55:42 +0200
Merged with the 'connection_profiles' branch.
MqttMonitor/MqttMonitorWidget.py | file | annotate | diff | comparison | revisions | |
MqttMonitor/MqttMonitorWidget.ui | file | annotate | diff | comparison | revisions |
--- a/MqttMonitor/MqttClient.py Sat Sep 08 16:51:39 2018 +0200 +++ b/MqttMonitor/MqttClient.py Sat Sep 08 16:55:42 2018 +0200 @@ -13,6 +13,8 @@ import paho.mqtt.client as mqtt +from Utilities.crypto import pwConvert + class MqttClient(QObject): """ @@ -158,6 +160,20 @@ self.__mqttClient.will_set(topic, payload=payload, qos=qos, retain=retain) + def setTLS(self, caCerts=None, certFile=None, keyFile=None): + """ + Public method to enable secure connections and set the TLS parameters. + + @param caCerts path to the Certificate Authority certificates file + @type str + @param certFile PEM encoded client certificate file + @type str + @param keyFile PEM encoded private key file + @type str + """ + self.__mqttClient.tls_set(ca_certs=caCerts, certfile=certFile, + keyfile=keyFile) + def startLoop(self): """ Public method to start the MQTT client loop. @@ -210,7 +226,8 @@ @param options dictionary containing the connection options. This dictionary should contain the keys "ClientId", "Keepalive", "CleanSession", "Username", "Password", "WillTopic", "WillMessage", - "WillQos", "WillRetain" + "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", "TlsClientCert", + "TlsClientKey" @type dict """ if options: @@ -226,8 +243,9 @@ # step 2: set username and password if parametersDict["Username"]: if parametersDict["Password"]: - self.setUserCredentials(parametersDict["Username"], - parametersDict["Password"]) + self.setUserCredentials( + parametersDict["Username"], + pwConvert(parametersDict["Password"], encode=False)) else: self.setUserCredentials(parametersDict["Username"]) @@ -243,7 +261,22 @@ parametersDict["WillQos"], parametersDict["WillRetain"]) - # step 4: connect to server + # step 4: set TLS parameters + if parametersDict["TlsEnable"]: + if parametersDict["TlsCaCert"] and \ + parametersDict["TlsClientCert"]: + # use self signed client certificate + self.setTLS(caCerts=parametersDict["TlsCaCert"], + certFile=parametersDict["TlsClientCert"], + keyFile=parametersDict["TlsClientKey"]) + elif parametersDict["TlsCaCert"]: + # use CA certificate file + self.setTLS(caCerts=parametersDict["TlsCaCert"]) + else: + # use default TLS configuration + self.setTLS() + + # step 5: connect to server self.connectToServer(host, port=port, keepalive=parametersDict["Keepalive"]) else: @@ -258,7 +291,8 @@ @return dictionary containing the default connection options. It has the keys "ClientId", "Keepalive", "CleanSession", "Username", - "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain" + "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain", + "TlsEnable", "TlsCaCert", "TlsClientCert", "TlsClientKey". @rtype dict """ return { @@ -271,6 +305,10 @@ "WillMessage": "", "WillQos": 0, "WillRetain": False, + "TlsEnable": False, + "TlsCaCert": "", + "TlsClientCert": "", + "TlsClientKey": "", } def reconnectToServer(self):
--- a/MqttMonitor/MqttConnectionOptionsDialog.py Sat Sep 08 16:51:39 2018 +0200 +++ b/MqttMonitor/MqttConnectionOptionsDialog.py Sat Sep 08 16:55:42 2018 +0200 @@ -12,8 +12,13 @@ from PyQt5.QtCore import pyqtSlot, QUuid from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton +from E5Gui import E5MessageBox +from E5Gui.E5PathPicker import E5PathPickerModes + from .Ui_MqttConnectionOptionsDialog import Ui_MqttConnectionOptionsDialog +from Utilities.crypto import pwConvert + class MqttConnectionOptionsDialog(QDialog, Ui_MqttConnectionOptionsDialog): """ @@ -28,8 +33,8 @@ @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". - @@type dict + "WillMessage", "WillQos", "WillRetain", "TlsEnable", "TlsCaCert". + @type dict @param parent reference to the parent widget @type QWidget """ @@ -38,7 +43,29 @@ self.__client = client + self.tlsCertsFilePicker.setMode(E5PathPickerModes.OpenFileMode) + self.tlsCertsFilePicker.setFilters( + self.tr("Certificate Files (*.crt *.pem);;All Files (*)")) + self.__populateDefaults(options=options) + + self.__updateOkButton() + + def __updateOkButton(self): + """ + Private method to update the enabled state of the OK button. + """ + if self.clientIdEdit.text() == "" and \ + not self.cleanSessionCheckBox.isChecked(): + enable = False + E5MessageBox.critical( + self, + self.tr("Invalid Connection Parameters"), + self.tr("""An empty Client ID requires a clean session.""")) + else: + enable = True + + self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable) @pyqtSlot() def on_generateIdButton_clicked(self): @@ -69,7 +96,7 @@ @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". + "WillQos", "WillRetain", "TlsEnable", "TlsCaCert". @type dict """ if options is None: @@ -82,13 +109,17 @@ # user credentials self.usernameEdit.setText(options["Username"]) - self.passwordEdit.setText(options["Password"]) + self.passwordEdit.setText(pwConvert(options["Password"], encode=False)) # last will and testament self.willQosSpinBox.setValue(options["WillQos"]) self.willRetainCheckBox.setChecked(options["WillRetain"]) self.willTopicEdit.setText(options["WillTopic"]) self.willMessageEdit.setPlainText(options["WillMessage"]) + + # TLS parameters + self.tlsEnableCheckBox.setChecked(options["TlsEnable"]) + self.tlsCertsFilePicker.setText(options["TlsCaCert"]) def getConnectionOptions(self): """ @@ -96,7 +127,8 @@ @return dictionary containing the connection options. It has the keys "ClientId", "Keepalive", "CleanSession", "Username", "Password", - "WillTopic", "WillMessage", "WillQos", "WillRetain". + "WillTopic", "WillMessage", "WillQos", "WillRetain", "TlsEnable", + "TlsCaCert". @rtype tuple of (int, dict) """ return { @@ -104,9 +136,31 @@ "Keepalive": self.keepaliveSpinBox.value(), "CleanSession": self.cleanSessionCheckBox.isChecked(), "Username": self.usernameEdit.text(), - "Password": self.passwordEdit.text(), + "Password": pwConvert(self.passwordEdit.text(), encode=True), "WillTopic": self.willTopicEdit.text(), "WillMessage": self.willMessageEdit.toPlainText(), "WillQos": self.willQosSpinBox.value(), "WillRetain": self.willRetainCheckBox.isChecked(), + "TlsEnable": self.tlsEnableCheckBox.isChecked(), + "TlsCaCert": self.tlsCertsFilePicker.text() } + + @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 Sat Sep 08 16:51:39 2018 +0200 +++ b/MqttMonitor/MqttConnectionOptionsDialog.ui Sat Sep 08 16:55:42 2018 +0200 @@ -16,7 +16,7 @@ <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"> @@ -73,6 +73,9 @@ <property name="singleStep"> <number>5</number> </property> + <property name="value"> + <number>60</number> + </property> </widget> </item> <item> @@ -92,6 +95,9 @@ </item> <item row="2" column="0" colspan="3"> <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> @@ -147,7 +153,7 @@ </property> <layout class="QGridLayout" name="gridLayout_3"> <item row="0" column="0"> - <widget class="QLineEdit" name="willTopicEdit"> + <widget class="E5ClearableLineEdit" name="willTopicEdit"> <property name="toolTip"> <string>Enter the topic of the last will</string> </property> @@ -200,6 +206,51 @@ </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="E5PathPicker" 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> + </widget> + </item> + <item> <widget class="QDialogButtonBox" name="buttonBox"> <property name="orientation"> <enum>Qt::Horizontal</enum> @@ -217,6 +268,12 @@ <extends>QLineEdit</extends> <header>E5Gui/E5LineEdit.h</header> </customwidget> + <customwidget> + <class>E5PathPicker</class> + <extends>QWidget</extends> + <header>E5Gui/E5PathPicker.h</header> + <container>1</container> + </customwidget> </customwidgets> <tabstops> <tabstop>clientIdEdit</tabstop> @@ -239,8 +296,8 @@ <slot>accept()</slot> <hints> <hint type="sourcelabel"> - <x>248</x> - <y>254</y> + <x>257</x> + <y>590</y> </hint> <hint type="destinationlabel"> <x>157</x> @@ -255,8 +312,8 @@ <slot>reject()</slot> <hints> <hint type="sourcelabel"> - <x>316</x> - <y>260</y> + <x>325</x> + <y>590</y> </hint> <hint type="destinationlabel"> <x>286</x> @@ -264,5 +321,21 @@ </hint> </hints> </connection> + <connection> + <sender>tlsEnableCheckBox</sender> + <signal>toggled(bool)</signal> + <receiver>tlsCertsFilePicker</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>82</x> + <y>523</y> + </hint> + <hint type="destinationlabel"> + <x>92</x> + <y>544</y> + </hint> + </hints> + </connection> </connections> </ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MqttMonitor/MqttConnectionProfilesDialog.py Sat Sep 08 16:55:42 2018 +0200 @@ -0,0 +1,699 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to edit the MQTT connection profiles. +""" + +from __future__ import unicode_literals + +import collections + +from PyQt5.QtCore import pyqtSlot, Qt, QUuid +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \ + QListWidgetItem, QInputDialog, QLineEdit + +from E5Gui import E5MessageBox +from E5Gui.E5PathPicker import E5PathPickerModes + +from .Ui_MqttConnectionProfilesDialog import Ui_MqttConnectionProfilesDialog + +import UI.PixmapCache +from Utilities.crypto import pwConvert + + +class MqttConnectionProfilesDialog(QDialog, Ui_MqttConnectionProfilesDialog): + """ + Class implementing a dialog to edit the MQTT connection profiles. + """ + def __init__(self, client, profiles, parent=None): + """ + Constructor + + @param client reference to the MQTT client object + @type MqttClient + @param profiles dictionary containing dictionaries containing the + connection parameters. Each entry must have the keys + "BrokerAddress", "BrokerPort", "ClientId", + "Keepalive", "CleanSession", "Username", "Password", "WillTopic", + "WillMessage", "WillQos", "WillRetain", "TlsEnable", "TlsCaCert", + "TlsClientCert", "TlsClientKey". + @type dict + @param parent reference to the parent widget + @type QWidget + """ + super(MqttConnectionProfilesDialog, self).__init__(parent) + self.setupUi(self) + + self.__client = client + + self.__profiles = collections.defaultdict(self.__defaultProfile) + self.__profiles.update(profiles) + self.__profilesChanged = False + + self.plusButton.setIcon(UI.PixmapCache.getIcon("plus.png")) + self.copyButton.setIcon(UI.PixmapCache.getIcon("editCopy.png")) + self.minusButton.setIcon(UI.PixmapCache.getIcon("minus.png")) + + self.tlsCertsFilePicker.setMode(E5PathPickerModes.OpenFileMode) + self.tlsCertsFilePicker.setFilters( + self.tr("Certificate Files (*.crt *.pem);;All Files (*)")) + self.tlsSelfSignedCertsFilePicker.setMode( + E5PathPickerModes.OpenFileMode) + self.tlsSelfSignedCertsFilePicker.setFilters( + self.tr("Certificate Files (*.crt *.pem);;All Files (*)")) + self.tlsSelfSignedClientCertFilePicker.setMode( + E5PathPickerModes.OpenFileMode) + self.tlsSelfSignedClientCertFilePicker.setFilters( + self.tr("Certificate Files (*.crt *.pem);;All Files (*)")) + self.tlsSelfSignedClientKeyFilePicker.setMode( + E5PathPickerModes.OpenFileMode) + self.tlsSelfSignedClientKeyFilePicker.setFilters( + self.tr("Key Files (*.key *.pem);;All Files (*)")) + + self.profileTabWidget.setCurrentIndex(0) + + if len(self.__profiles) == 0: + self.minusButton.setEnabled(False) + self.copyButton.setEnabled(False) + + self.profileFrame.setEnabled(False) + self.__populatingProfile = False + self.__deletingProfile = False + + self.__populateProfilesList() + + @pyqtSlot(str) + def on_profileEdit_textChanged(self, name): + """ + Private slot to handle changes of the profile name. + + @param name name of the profile + @type str + """ + self.__updateApplyButton() + + @pyqtSlot(QAbstractButton) + def on_profileButtonBox_clicked(self, button): + """ + Private slot handling presses of the profile buttons. + + @param button reference to the pressed button + @type QAbstractButton + """ + if button == self.profileButtonBox.button(QDialogButtonBox.Apply): + currentProfile = self.__applyProfile() + self.__populateProfilesList(currentProfile) + + elif button == self.profileButtonBox.button(QDialogButtonBox.Reset): + self.__resetProfile() + + elif button == self.profileButtonBox.button( + QDialogButtonBox.RestoreDefaults): + self.__populateProfileDefault() + + @pyqtSlot(QListWidgetItem, QListWidgetItem) + def on_profilesList_currentItemChanged(self, current, previous): + """ + Private slot to handle a change of the current profile. + + @param current new current item + @type QListWidgetItem + @param previous previous current item + @type QListWidgetItem + """ + self.minusButton.setEnabled(current is not None) + self.copyButton.setEnabled(current is not None) + + if current is not previous: + if not self.__deletingProfile and self.__isChangedProfile(): + # modified profile belongs to previous + yes = E5MessageBox.yesNo( + self, + self.tr("Changed Connection Profile"), + self.tr("""The current profile has unsaved changes.""" + """ Shall these be saved?"""), + icon=E5MessageBox.Warning, + yesDefault=True) + if yes: + self.__applyProfile() + + if current: + profileName = current.text() + self.__populateProfile(profileName) + else: + self.__clearProfile() + + @pyqtSlot() + def on_plusButton_clicked(self): + """ + Private slot to add a new empty profile entry. + """ + profileName, ok = QInputDialog.getText( + self, + self.tr("New Connection Profile"), + self.tr("Enter name for the new Connection Profile:"), + QLineEdit.Normal) + if ok and bool(profileName): + if profileName in self.__profiles: + E5MessageBox.warning( + self, + self.tr("New Connection Profile"), + self.tr("""<p>A connection named <b>{0}</b> exists""" + """ already. Aborting...</p>""").format( + profileName)) + else: + itm = QListWidgetItem(profileName, self.profilesList) + self.profilesList.setCurrentItem(itm) + self.brokerAddressEdit.setFocus(Qt.OtherFocusReason) + + @pyqtSlot() + def on_copyButton_clicked(self): + """ + Private slot to copy the selected profile entry. + """ + itm = self.profilesList.currentItem() + if itm: + profileName = itm.text() + newProfileName, ok = QInputDialog.getText( + self, + self.tr("Copy Connection Profile"), + self.tr("Enter name for the copied Connection Profile:"), + QLineEdit.Normal) + if ok and bool(newProfileName): + if newProfileName in self.__profiles: + E5MessageBox.warning( + self, + self.tr("Copy Connection Profile"), + self.tr("""<p>A connection named <b>{0}</b> exists""" + """ already. Aborting...</p>""").format( + newProfileName)) + else: + profile = self.__defaultProfile() + profile.update(self.__profiles[profileName]) + self.__profiles[newProfileName] = profile + + itm = QListWidgetItem(newProfileName, self.profilesList) + self.profilesList.setCurrentItem(itm) + self.brokerAddressEdit.setFocus(Qt.OtherFocusReason) + + @pyqtSlot() + def on_minusButton_clicked(self): + """ + Private slot to delete the selected entry. + """ + itm = self.profilesList.currentItem() + if itm: + profileName = itm.text() + yes = E5MessageBox.yesNo( + self, + self.tr("Delete Connection Profile"), + self.tr("""<p>Shall the Connection Profile <b>{0}</b>""" + """ really be deleted?</p>""").format(profileName) + ) + if yes: + self.__deletingProfile = True + del self.__profiles[profileName] + self.__profilesChanged = True + self.__populateProfilesList() + self.__deletingProfile = False + + self.profilesList.setFocus(Qt.OtherFocusReason) + + def getProfiles(self): + """ + Public method to return a dictionary of profiles. + + @return dictionary containing dictionaries containing the defined + connection profiles. Each entry have the keys "BrokerAddress", + "BrokerPort", "ClientId", "Keepalive", "CleanSession", "Username", + "Password", "WillTopic", "WillMessage", "WillQos", "WillRetain", + "TlsEnable", "TlsCaCert", "TlsClientCert", "TlsClientKey". + @rtype dict + """ + profilesDict = {} + profilesDict.update(self.__profiles) + return profilesDict + + def __applyProfile(self): + """ + Private method to apply the entered data to the list of profiles. + + @return name of the applied profile + @rtype str + """ + profileName = self.profileEdit.text() + profile = { + "BrokerAddress": self.brokerAddressEdit.text(), + "BrokerPort": self.brokerPortSpinBox.value(), + "ClientId": self.clientIdEdit.text(), + "Keepalive": self.keepaliveSpinBox.value(), + "CleanSession": self.cleanSessionCheckBox.isChecked(), + "Username": self.usernameEdit.text(), + "Password": pwConvert(self.passwordEdit.text(), encode=True), + "WillTopic": self.willTopicEdit.text(), + "WillMessage": self.willMessageEdit.toPlainText(), + "WillQos": self.willQosSpinBox.value(), + "WillRetain": self.willRetainCheckBox.isChecked(), + "TlsEnable": self.tlsGroupBox.isChecked(), + "TlsCaCert": "", + "TlsClientCert": "", + "TlsClientKey": "", + } + if profile["TlsEnable"]: + if self.tlsCertsFileButton.isChecked(): + profile["TlsCaCert"] = self.tlsCertsFilePicker.text() + elif self.tlsSelfSignedCertsButton.isChecked(): + profile["TlsCaCert"] = \ + self.tlsSelfSignedCertsFilePicker.text() + profile["TlsClientCert"] = \ + self.tlsSelfSignedClientCertFilePicker.text() + profile["TlsClientKey"] = \ + self.tlsSelfSignedClientKeyFilePicker.text() + + self.__profiles[profileName] = profile + self.__profilesChanged = True + + return profileName + + def __defaultProfile(self): + """ + Private method to populate non-existing profile items. + + @return default dictionary entry + @rtype dict + """ + defaultProfile = self.__client.defaultConnectionOptions() + defaultProfile["BrokerAddress"] = "" + if defaultProfile["TlsEnable"]: + defaultProfile["BrokerPort"] = 8883 + else: + defaultProfile["BrokerPort"] = 1883 + + return defaultProfile + + def __populateProfilesList(self, currentProfile=""): + """ + Private method to populate the list of defined profiles. + + @param currentProfile name of the current profile + @type str + """ + if not currentProfile: + currentItem = self.profilesList.currentItem() + if currentItem: + currentProfile = currentItem.text() + + self.profilesList.clear() + self.profilesList.addItems(sorted(self.__profiles.keys())) + + if currentProfile: + items = self.profilesList.findItems( + currentProfile, Qt.MatchExactly) + if items: + self.profilesList.setCurrentItem(items[0]) + + if len(self.__profiles) == 0: + self.profileFrame.setEnabled(False) + + def __populateProfile(self, profileName): + """ + Private method to populate the profile data entry fields. + + @param profileName name of the profile to get data from + @type str + """ + profile = self.__defaultProfile() + if profileName: + profile.update(self.__profiles[profileName]) + + self.__populatingProfile = True + if profileName is not None: + self.profileEdit.setText(profileName) + self.brokerAddressEdit.setText(profile["BrokerAddress"]) + self.brokerPortSpinBox.setValue(profile["BrokerPort"]) + self.clientIdEdit.setText(profile["ClientId"]) + self.keepaliveSpinBox.setValue(profile["Keepalive"]) + self.cleanSessionCheckBox.setChecked(profile["CleanSession"]) + self.usernameEdit.setText(profile["Username"]) + self.passwordEdit.setText(pwConvert(profile["Password"], encode=False)) + self.willTopicEdit.setText(profile["WillTopic"]) + self.willMessageEdit.setPlainText(profile["WillMessage"]) + self.willQosSpinBox.setValue(profile["WillQos"]) + self.willRetainCheckBox.setChecked(profile["WillRetain"]) + self.tlsGroupBox.setChecked(profile["TlsEnable"]) + if profile["TlsCaCert"] and profile["TlsClientCert"]: + self.tlsSelfSignedCertsButton.setChecked(True) + self.tlsSelfSignedCertsFilePicker.setText(profile["TlsCaCert"]) + self.tlsSelfSignedClientCertFilePicker.setText( + profile["TlsClientCert"]) + self.tlsSelfSignedClientKeyFilePicker.setText( + profile["TlsClientKey"]) + elif profile["TlsCaCert"]: + self.tlsCertsFileButton.setChecked(True) + self.tlsCertsFilePicker.setText(profile["TlsCaCert"]) + else: + self.tlsDefaultCertsButton.setChecked(True) + self.__populatingProfile = False + + self.profileFrame.setEnabled(True) + self.__updateApplyButton() + + def __clearProfile(self): + """ + Private method to clear the profile data entry fields. + """ + self.__populatingProfile = True + self.profileEdit.setText("") + self.brokerAddressEdit.setText("") + self.brokerPortSpinBox.setValue(1883) + self.clientIdEdit.setText("") + self.keepaliveSpinBox.setValue(60) + self.cleanSessionCheckBox.setChecked(True) + self.usernameEdit.setText("") + self.passwordEdit.setText("") + self.willTopicEdit.setText("") + self.willMessageEdit.setPlainText("") + self.willQosSpinBox.setValue(0) + self.willRetainCheckBox.setChecked(False) + self.tlsGroupBox.setChecked(False) + self.tlsDefaultCertsButton.setChecked(True) + self.tlsCertsFileButton.setChecked(True) + self.tlsCertsFilePicker.setText("") + self.tlsSelfSignedCertsButton.setChecked(False) + self.tlsSelfSignedCertsFilePicker.setText("") + self.tlsSelfSignedClientCertFilePicker.setText("") + self.tlsSelfSignedClientKeyFilePicker.setText("") + self.__populatingProfile = False + + self.profileFrame.setEnabled(False) + self.__updateApplyButton() + + def __resetProfile(self): + """ + Private method to reset the profile data entry fields to their stored + values. + """ + profileName = self.profileEdit.text() + if profileName in self.__profiles: + self.__populateProfile(profileName) + + def __populateProfileDefault(self): + """ + Private method to populate the profile data entry fields with default + profile values. + """ + self.__populateProfile(None) + + def __isChangedProfile(self): + """ + Private method to check, if the currently shown profile contains some + changed data. + + @return flag indicating changed data + @type bool + """ + profileName = self.profileEdit.text() + if profileName == "": + return False + + elif profileName in self.__profiles: + profile = self.__defaultProfile() + profile.update(self.__profiles[profileName]) + changed = ( + self.brokerAddressEdit.text() != profile["BrokerAddress"] or + self.brokerPortSpinBox.value() != profile["BrokerPort"] or + self.clientIdEdit.text() != profile["ClientId"] or + self.keepaliveSpinBox.value() != profile["Keepalive"] or + self.cleanSessionCheckBox.isChecked() != + profile["CleanSession"] or + self.usernameEdit.text() != profile["Username"] or + self.passwordEdit.text() != + pwConvert(profile["Password"], encode=False) or + self.willTopicEdit.text() != profile["WillTopic"] or + self.willMessageEdit.toPlainText() != profile["WillMessage"] or + self.willQosSpinBox.value() != profile["WillQos"] or + self.willRetainCheckBox.isChecked() != profile["WillRetain"] or + self.tlsGroupBox.isChecked() != profile["TlsEnable"] + ) + # check TLS stuff only, if not yet changed + if not changed: + if self.tlsCertsFileButton.isChecked(): + changed |= ( + self.tlsCertsFilePicker.text() != profile["TlsCaCert"] + ) + elif self.tlsSelfSignedCertsButton.isChecked(): + changed |= ( + self.tlsSelfSignedCertsFilePicker.text() != + profile["TlsCaCert"] or + self.tlsSelfSignedClientCertFilePicker.text() != + profile["TlsClientCert"] or + self.tlsSelfSignedClientKeyFilePicker.text() != + profile["TlsClientKey"] + ) + return changed + + else: + return True + + def __updateApplyButton(self): + """ + Private method to set the state of the Apply button. + """ + # condition 1: profile name and broker address need to be given + enable = (bool(self.profileEdit.text()) and + bool(self.brokerAddressEdit.text())) + + # condition 2: if client ID is empty, clean session must be selected + if not self.__populatingProfile: + if self.clientIdEdit.text() == "" and \ + not self.cleanSessionCheckBox.isChecked(): + enable = False + E5MessageBox.critical( + self, + self.tr("Invalid Connection Parameters"), + self.tr("An empty Client ID requires a clean session.")) + + if self.tlsGroupBox.isChecked(): + if self.tlsCertsFileButton.isChecked(): + # condition 3a: if CA certificates file shall be used, it must + # be given + enable &= bool(self.tlsCertsFilePicker.text()) + elif self.tlsSelfSignedCertsButton.isChecked(): + # condition 3b: if client certificates shall be used, all files + # must be given + enable &= ( + bool(self.tlsSelfSignedCertsFilePicker.text()) and + bool(self.tlsSelfSignedClientCertFilePicker.text()) and + bool(self.tlsSelfSignedClientKeyFilePicker.text()) + ) + + self.profileButtonBox.button(QDialogButtonBox.Apply).setEnabled(enable) + + @pyqtSlot(str) + def on_brokerAddressEdit_textChanged(self, address): + """ + Private slot handling a change of the broker address. + + @param address broker address + @type str + """ + self.__updateApplyButton() + + @pyqtSlot() + def on_generateIdButton_clicked(self): + """ + Private slot to generate a client ID. + """ + uuid = QUuid.createUuid() + self.clientIdEdit.setText(uuid.toString(QUuid.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.__updateApplyButton() + + @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.__updateApplyButton() + + @pyqtSlot(str) + def on_tlsCertsFilePicker_textChanged(self, path): + """ + Private slot handling a change of the TLS CA certificates file. + + @param path file path + @type str + """ + self.__updateApplyButton() + + @pyqtSlot(str) + def on_tlsSelfSignedCertsFilePicker_textChanged(self, path): + """ + Private slot handling a change of the TLS CA certificates file. + + @param path file path + @type str + """ + self.__updateApplyButton() + + @pyqtSlot(str) + def on_tlsSelfSignedClientCertFilePicker_textChanged(self, path): + """ + Private slot handling a change of the TLS client certificate file. + + @param path file path + @type str + """ + self.__updateApplyButton() + + @pyqtSlot(str) + def on_tlsSelfSignedClientKeyFilePicker_textChanged(self, path): + """ + Private slot handling a change of the TLS client key file. + + @param path file path + @type str + """ + self.__updateApplyButton() + + @pyqtSlot(bool) + def on_tlsGroupBox_toggled(self, checked): + """ + Private slot handling the selection of TLS mode. + + @param checked state of the selection + @type bool + """ + if checked and self.brokerPortSpinBox.value() == 1883: + # port is still standard non-TLS port + yes = E5MessageBox.yesNo( + self, + self.tr("SSL/TLS Enabled"), + self.tr( + """Encrypted connection using SSL/TLS has been enabled.""" + """ However, the broker port is still the default""" + """ unencrypted port (port 1883). Shall this be""" + """ changed?"""), + icon=E5MessageBox.Warning, + yesDefault=True) + if yes: + self.brokerPortSpinBox.setValue(8883) + elif not checked and self.brokerPortSpinBox.value() == 8883: + # port is still standard TLS port + yes = E5MessageBox.yesNo( + self, + self.tr("SSL/TLS Disabled"), + self.tr( + """Encrypted connection using SSL/TLS has been disabled.""" + """ However, the broker port is still the default""" + """ encrypted port (port 8883). Shall this be""" + """ changed?"""), + icon=E5MessageBox.Warning, + yesDefault=True) + if yes: + self.brokerPortSpinBox.setValue(1883) + + self.__updateApplyButton() + + @pyqtSlot(bool) + def on_tlsDefaultCertsButton_toggled(self, checked): + """ + Private slot handling the selection of using the default + certificates file. + + @param checked state of the selection + @type bool + """ + self.__updateApplyButton() + + @pyqtSlot(bool) + def on_tlsCertsFileButton_toggled(self, checked): + """ + Private slot handling the selection of using a non-default + certificates file. + + @param checked state of the selection + @type bool + """ + self.__updateApplyButton() + + @pyqtSlot(bool) + def on_tlsSelfSignedCertsButton_toggled(self, checked): + """ + Private slot handling the selection of using self signed + client certificate and key files. + + @param checked state of the selection + @type bool + """ + self.__updateApplyButton() + + @pyqtSlot() + def reject(self): + """ + Public slot to reject the dialog changes. + """ + if self.__isChangedProfile(): + button = E5MessageBox.warning( + self, + self.tr("Changed Connection Profile"), + self.tr("""The current profile has unsaved changes. Shall""" + """ these be saved?"""), + E5MessageBox.StandardButtons( + E5MessageBox.Discard | + E5MessageBox.Save), + E5MessageBox.Save) + if button == E5MessageBox.Save: + self.__applyProfile() + return + + if self.__profilesChanged: + button = E5MessageBox.warning( + self, + self.tr("Changed Connection Profiles"), + self.tr("""The list of connection profiles has unsaved""" + """ changes."""), + E5MessageBox.StandardButtons( + E5MessageBox.Abort | + E5MessageBox.Discard | + E5MessageBox.Save), + E5MessageBox.Save) + if button == E5MessageBox.Save: + super(MqttConnectionProfilesDialog, self).accept() + return + elif button == E5MessageBox.Abort: + return + + super(MqttConnectionProfilesDialog, self).reject() + + @pyqtSlot() + def accept(self): + """ + Public slot to accept the dialog. + """ + if self.__isChangedProfile(): + yes = E5MessageBox.yesNo( + self, + self.tr("Changed Connection Profile"), + self.tr("""The current profile has unsaved changes. Shall""" + """ these be saved?"""), + icon=E5MessageBox.Warning, + yesDefault=True) + if yes: + self.__applyProfile() + + super(MqttConnectionProfilesDialog, self).accept()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MqttMonitor/MqttConnectionProfilesDialog.ui Sat Sep 08 16:55:42 2018 +0200 @@ -0,0 +1,760 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MqttConnectionProfilesDialog</class> + <widget class="QDialog" name="MqttConnectionProfilesDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>MQTT Connection Profiles</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="childrenCollapsible"> + <bool>false</bool> + </property> + <widget class="QFrame" name="profileListFrame"> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QListWidget" name="profilesList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QToolButton" name="plusButton"> + <property name="toolTip"> + <string>Press to add a new profile</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="copyButton"> + <property name="toolTip"> + <string>Press to copy the selected profile</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="minusButton"> + <property name="toolTip"> + <string>Press to delete the selected profile</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QFrame" name="profileFrame"> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Profile Name:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="profileEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Broker Address:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="E5ClearableLineEdit" name="brokerAddressEdit"> + <property name="toolTip"> + <string>Enter the broker server address</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Broker Port:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QSpinBox" name="brokerPortSpinBox"> + <property name="toolTip"> + <string>Enter the broker port number (default 1883)</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="maximum"> + <number>65535</number> + </property> + <property name="value"> + <number>1883</number> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>318</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Client ID:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="E5ClearableLineEdit" name="clientIdEdit"> + <property name="toolTip"> + <string>Enter the ID string for this client</string> + </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 row="5" column="0" colspan="2"> + <widget class="Line" name="line_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTabWidget" name="profileTabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="generalTab"> + <attribute name="title"> + <string>General</string> + </attribute> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Keep Alive Interval:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <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> + <spacer name="horizontalSpacer_3"> + <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 row="1" column="0" colspan="2"> + <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 row="2" column="1"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>227</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_3"> + <item row="0" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>User Name:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="E5ClearableLineEdit" name="usernameEdit"> + <property name="toolTip"> + <string>Enter the user name</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Password:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="E5ClearableLineEdit" name="passwordEdit"> + <property name="toolTip"> + <string>Enter the password</string> + </property> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item row="2" column="0"> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>228</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_4"> + <item row="0" column="0"> + <widget class="E5ClearableLineEdit" name="willTopicEdit"> + <property name="toolTip"> + <string>Enter the topic of the last will</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <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="2"> + <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="3"> + <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> + <widget class="QWidget" name="tlsTab"> + <attribute name="title"> + <string>SSL/TLS</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <item> + <widget class="QGroupBox" name="tlsGroupBox"> + <property name="toolTip"> + <string>Select to enable SSL/TLS connections</string> + </property> + <property name="title"> + <string>SSL/TLS Enabled</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + <property name="checked"> + <bool>false</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QRadioButton" name="tlsDefaultCertsButton"> + <property name="toolTip"> + <string>Select to use the default certificate file of the client</string> + </property> + <property name="text"> + <string>CA signed server certificate</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="tlsCertsFileButton"> + <property name="toolTip"> + <string>Select to use a specific certificate file</string> + </property> + <property name="text"> + <string>CA certificate file</string> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="tlsCertsFileWidget" native="true"> + <property name="enabled"> + <bool>false</bool> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_6"> + <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> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>25</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>CA File:</string> + </property> + </widget> + </item> + <item> + <widget class="E5PathPicker" name="tlsCertsFilePicker" native="true"> + <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</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QRadioButton" name="tlsSelfSignedCertsButton"> + <property name="toolTip"> + <string>Select to use a self signed client certificate</string> + </property> + <property name="text"> + <string>Self signed certificates</string> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="tlsSelfSignedFilesWidget" native="true"> + <property name="enabled"> + <bool>false</bool> + </property> + <layout class="QGridLayout" name="gridLayout_5"> + <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 row="0" column="0"> + <spacer name="horizontalSpacer_5"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>25</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>CA File:</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="E5PathPicker" name="tlsSelfSignedCertsFilePicker" native="true"> + <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</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Client Certificate File:</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="E5PathPicker" name="tlsSelfSignedClientCertFilePicker" native="true"> + <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 client certificate file</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="label_11"> + <property name="text"> + <string>Client Key File:</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <widget class="E5PathPicker" name="tlsSelfSignedClientKeyFilePicker" native="true"> + <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 client key file</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>128</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="profileButtonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Apply|QDialogButtonBox::Reset|QDialogButtonBox::RestoreDefaults</set> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <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> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + <customwidget> + <class>E5PathPicker</class> + <extends>QWidget</extends> + <header>E5Gui/E5PathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>profilesList</tabstop> + <tabstop>plusButton</tabstop> + <tabstop>copyButton</tabstop> + <tabstop>minusButton</tabstop> + <tabstop>profileEdit</tabstop> + <tabstop>brokerAddressEdit</tabstop> + <tabstop>brokerPortSpinBox</tabstop> + <tabstop>clientIdEdit</tabstop> + <tabstop>generateIdButton</tabstop> + <tabstop>profileTabWidget</tabstop> + <tabstop>keepaliveSpinBox</tabstop> + <tabstop>cleanSessionCheckBox</tabstop> + <tabstop>usernameEdit</tabstop> + <tabstop>passwordEdit</tabstop> + <tabstop>willTopicEdit</tabstop> + <tabstop>willMessageEdit</tabstop> + <tabstop>willQosSpinBox</tabstop> + <tabstop>willRetainCheckBox</tabstop> + <tabstop>tlsGroupBox</tabstop> + <tabstop>tlsDefaultCertsButton</tabstop> + <tabstop>tlsCertsFileButton</tabstop> + <tabstop>tlsCertsFilePicker</tabstop> + <tabstop>tlsSelfSignedCertsButton</tabstop> + <tabstop>tlsSelfSignedCertsFilePicker</tabstop> + <tabstop>tlsSelfSignedClientCertFilePicker</tabstop> + <tabstop>tlsSelfSignedClientKeyFilePicker</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>MqttConnectionProfilesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>227</x> + <y>579</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>MqttConnectionProfilesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>295</x> + <y>585</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>tlsCertsFileButton</sender> + <signal>toggled(bool)</signal> + <receiver>tlsCertsFileWidget</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>367</x> + <y>238</y> + </hint> + <hint type="destinationlabel"> + <x>357</x> + <y>252</y> + </hint> + </hints> + </connection> + <connection> + <sender>tlsSelfSignedCertsButton</sender> + <signal>toggled(bool)</signal> + <receiver>tlsSelfSignedFilesWidget</receiver> + <slot>setEnabled(bool)</slot> + <hints> + <hint type="sourcelabel"> + <x>387</x> + <y>287</y> + </hint> + <hint type="destinationlabel"> + <x>466</x> + <y>305</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- a/MqttMonitor/MqttMonitorWidget.py Sat Sep 08 16:51:39 2018 +0200 +++ b/MqttMonitor/MqttMonitorWidget.py Sat Sep 08 16:55:42 2018 +0200 @@ -16,6 +16,7 @@ import os import collections +import copy from PyQt5.QtCore import pyqtSlot, QTimer from PyQt5.QtGui import QTextCursor @@ -61,6 +62,10 @@ self.brokerWidget.setCurrentIndex(0) + self.__connectionModeProfile = True + self.__setConnectionMode(True) # initial mode is 'profile connection' + self.__populateProfileComboBox() + self.connectButton.setIcon(UI.PixmapCache.getIcon("ircConnect.png")) self.brokerConnectionOptionsButton.setIcon(UI.PixmapCache.getIcon( os.path.join("MqttMonitor", "icons", "connectionOptions.png"))) @@ -283,6 +288,14 @@ self.brokerStatusLabel.show() QTimer.singleShot(5000, self.brokerStatusLabel.hide) + @pyqtSlot() + def on_modeButton_clicked(self): + """ + Private slot to switch between connection profiles and direct + connection mode. + """ + self.__setConnectionMode(not self.__connectionModeProfile) + @pyqtSlot(str) def on_brokerComboBox_editTextChanged(self, host): """ @@ -291,21 +304,41 @@ @param host host name of the broker @type str """ - if not self.__connectedToBroker and not host: - self.connectButton.setEnabled(False) - else: - self.connectButton.setEnabled(True) + self.__setConnectButtonState() @pyqtSlot() def on_brokerConnectionOptionsButton_clicked(self): """ - Private slot to show a dialog to modify connection options. + Private slot to show a dialog to modify connection options or a + dialog to edit connection profiles. """ - from .MqttConnectionOptionsDialog import MqttConnectionOptionsDialog - dlg = MqttConnectionOptionsDialog( - self.__client, self.__connectionOptions, parent=self) - if dlg.exec_() == QDialog.Accepted: - self.__connectionOptions = dlg.getConnectionOptions() + if self.__connectionModeProfile: + from .MqttConnectionProfilesDialog import \ + MqttConnectionProfilesDialog + dlg = MqttConnectionProfilesDialog( + self.__client, self.__plugin.getPreferences("BrokerProfiles"), + parent=self) + if dlg.exec_() == QDialog.Accepted: + profilesDict = dlg.getProfiles() + self.__plugin.setPreferences("BrokerProfiles", profilesDict) + self.__populateProfileComboBox() + else: + from .MqttConnectionOptionsDialog import \ + MqttConnectionOptionsDialog + dlg = MqttConnectionOptionsDialog( + self.__client, self.__connectionOptions, parent=self) + if dlg.exec_() == QDialog.Accepted: + self.__connectionOptions = dlg.getConnectionOptions() + if self.__connectionOptions["TlsEnable"]: + port = self.brokerPortComboBox.currentText().strip() + if port == "1883": + # it is default non-encrypted port => set to TLS port + self.brokerPortComboBox.setEditText("8883") + else: + port = self.brokerPortComboBox.currentText().strip() + if port == "8883": + # it is default TLS port => set to non-encrypted port + self.brokerPortComboBox.setEditText("1883") @pyqtSlot() def on_connectButton_clicked(self): @@ -315,20 +348,10 @@ if self.__connectedToBroker: self.__client.disconnectFromServer() else: - host = self.brokerComboBox.currentText() - port = self.brokerPortComboBox.currentText().strip() - try: - port = int(port) - except ValueError: - # use standard port at 1883 - port = 1883 - if host: - self.__addBrokerToRecent(host, port) - if self.__connectionOptions is None: - self.__client.connectToServer(host, port=port) - else: - self.__client.connectToServerWithOptions( - host, port=port, options=self.__connectionOptions) + if self.__connectionModeProfile: + self.__profileConnectToBroker() + else: + self.__directConnectToBroker() @pyqtSlot(str) def on_subscribeTopicEdit_textChanged(self, topic): @@ -502,6 +525,8 @@ # step 2a: populate the broker name list self.brokerComboBox.addItems([b[0].strip() for b in brokerList]) + self.__setConnectButtonState() + # step 2b: populate the broker ports list if brokerList: currentPort = brokerList[0][1] @@ -515,6 +540,17 @@ index = self.brokerPortComboBox.findText(currentPortStr) self.brokerPortComboBox.setCurrentIndex(index) + def __populateProfileComboBox(self): + """ + Private method to populate the profiles selection box. + """ + profilesDict = self.__plugin.getPreferences("BrokerProfiles") + + self.profileComboBox.clear() + self.profileComboBox.addItems(sorted(profilesDict.keys())) + + self.__setConnectButtonState() + def __updateUnsubscribeTopicComboBox(self): """ Private method to update the unsubcribe topic combo box. @@ -630,3 +666,66 @@ "5min": "-", "15min": "-", } + + def __setConnectionMode(self, profileMode): + """ + Private method to set the connection mode. + + @param profileMode flag indicating the profile connection mode + @type bool + """ + self.__connectionModeProfile = profileMode + if profileMode: + self.modeButton.setIcon(UI.PixmapCache.getIcon( + os.path.join("MqttMonitor", "icons", "profiles.png"))) + else: + self.modeButton.setIcon(UI.PixmapCache.getIcon( + os.path.join("MqttMonitor", "icons", "quickopen.png"))) + + self.profileComboBox.setVisible(profileMode) + self.brokerConnectionWidget.setVisible(not profileMode) + self.__setConnectButtonState() + + def __setConnectButtonState(self): + """ + Private method to set the enabled state of the connect button. + """ + if self.__connectionModeProfile: + self.connectButton.setEnabled( + bool(self.profileComboBox.currentText())) + else: + self.connectButton.setEnabled( + bool(self.brokerComboBox.currentText())) + + def __directConnectToBroker(self): + """ + Private method to connect to the broker with entered data. + """ + host = self.brokerComboBox.currentText() + port = self.brokerPortComboBox.currentText().strip() + try: + port = int(port) + except ValueError: + # use standard port at 1883 + port = 1883 + if host: + self.__addBrokerToRecent(host, port) + if self.__connectionOptions is None: + self.__client.connectToServer(host, port=port) + else: + self.__client.connectToServerWithOptions( + host, port=port, options=self.__connectionOptions) + + def __profileConnectToBroker(self): + """ + Private method to connect to the broker with selected profile. + """ + profileName = self.profileComboBox.currentText() + if profileName: + profilesDict = self.__plugin.getPreferences("BrokerProfiles") + profile = copy.copy(profilesDict[profileName]) # play it save + host = profile["BrokerAddress"] + port = profile["BrokerPort"] + + self.__client.connectToServerWithOptions(host, port=port, + options=profile)
--- a/MqttMonitor/MqttMonitorWidget.ui Sat Sep 08 16:51:39 2018 +0200 +++ b/MqttMonitor/MqttMonitorWidget.ui Sat Sep 08 16:55:42 2018 +0200 @@ -35,24 +35,89 @@ </layout> </item> <item> - <widget class="QGroupBox" name="groupBox"> + <widget class="QGroupBox" name="brokerGroupBox"> <property name="title"> <string>Broker</string> </property> <layout class="QGridLayout" name="gridLayout"> <item row="0" column="0"> - <widget class="E5ClearableComboBox" name="brokerComboBox"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> + <widget class="QToolButton" name="modeButton"> + <property name="toolTip"> + <string>Press to switch the mode between profiles and direct connection</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_15"> + <property name="spacing"> + <number>0</number> </property> + <item> + <widget class="QComboBox" name="profileComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the profile to be used to connect to the broker</string> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="brokerConnectionWidget" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_14"> + <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="E5ClearableComboBox" name="brokerComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Enter the host name of the broker</string> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="brokerPortComboBox"> + <property name="toolTip"> + <string>Enter the broker port to connect to</string> + </property> + <property name="editable"> + <bool>true</bool> + </property> + <property name="sizeAdjustPolicy"> + <enum>QComboBox::AdjustToContents</enum> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item row="0" column="2"> + <widget class="QToolButton" name="brokerConnectionOptionsButton"> <property name="toolTip"> - <string>Enter the host name of the broker</string> - </property> - <property name="editable"> - <bool>true</bool> + <string>Press to open a dialog to enter connection options</string> </property> </widget> </item> @@ -73,26 +138,6 @@ </property> </widget> </item> - <item row="0" column="1"> - <widget class="QComboBox" name="brokerPortComboBox"> - <property name="toolTip"> - <string>Enter the broker port to connect to</string> - </property> - <property name="editable"> - <bool>true</bool> - </property> - <property name="sizeAdjustPolicy"> - <enum>QComboBox::AdjustToContents</enum> - </property> - </widget> - </item> - <item row="0" column="2"> - <widget class="QToolButton" name="brokerConnectionOptionsButton"> - <property name="toolTip"> - <string>Press to open a dialog to enter connection options</string> - </property> - </widget> - </item> </layout> </widget> </item> @@ -434,8 +479,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"> @@ -1169,6 +1214,8 @@ </customwidget> </customwidgets> <tabstops> + <tabstop>modeButton</tabstop> + <tabstop>profileComboBox</tabstop> <tabstop>brokerComboBox</tabstop> <tabstop>brokerPortComboBox</tabstop> <tabstop>brokerConnectionOptionsButton</tabstop> @@ -1180,9 +1227,10 @@ <tabstop>unsubscribeTopicComboBox</tabstop> <tabstop>unsubscribeButton</tabstop> <tabstop>publishTopicComboBox</tabstop> + <tabstop>publishPayloadEdit</tabstop> <tabstop>publishQosSpinBox</tabstop> <tabstop>publishRetainCheckBox</tabstop> - <tabstop>publishPayloadEdit</tabstop> + <tabstop>publishClearButton</tabstop> <tabstop>publishButton</tabstop> <tabstop>messagesEdit</tabstop> <tabstop>messagesClearButton</tabstop>
--- a/PluginMqttMonitor.e4p Sat Sep 08 16:51:39 2018 +0200 +++ b/PluginMqttMonitor.e4p Sat Sep 08 16:55:42 2018 +0200 @@ -18,6 +18,7 @@ <Sources> <Source>MqttMonitor/MqttClient.py</Source> <Source>MqttMonitor/MqttConnectionOptionsDialog.py</Source> + <Source>MqttMonitor/MqttConnectionProfilesDialog.py</Source> <Source>MqttMonitor/MqttMonitorWidget.py</Source> <Source>MqttMonitor/__init__.py</Source> <Source>PluginMqttMonitor.py</Source> @@ -25,6 +26,7 @@ </Sources> <Forms> <Form>MqttMonitor/MqttConnectionOptionsDialog.ui</Form> + <Form>MqttMonitor/MqttConnectionProfilesDialog.ui</Form> <Form>MqttMonitor/MqttMonitorWidget.ui</Form> </Forms> <Translations>
--- a/PluginMqttMonitor.py Sat Sep 08 16:51:39 2018 +0200 +++ b/PluginMqttMonitor.py Sat Sep 08 16:55:42 2018 +0200 @@ -94,6 +94,8 @@ self.__defaults = { "RecentBrokersWithPort": "[]", # JSON formatted empty list + "BrokerProfiles": "{}", # JSON formatted empty dict + # __IGNORE_WARNING_M613__ } self.__translator = None @@ -209,7 +211,7 @@ @param key the key of the value to get @return the requested setting """ - if key in ["RecentBrokersWithPort"]: + if key in ["RecentBrokersWithPort", "BrokerProfiles"]: return json.loads(Preferences.Prefs.settings.value( self.PreferencesKey + "/" + key, self.__defaults[key])) else: @@ -223,7 +225,7 @@ @param key the key of the setting to be set (string) @param value the value to be set """ - if key in ["RecentBrokersWithPort"]: + if key in ["RecentBrokersWithPort", "BrokerProfiles"]: Preferences.Prefs.settings.setValue( self.PreferencesKey + "/" + key, json.dumps(value)) else: